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/index.js CHANGED
@@ -1,3130 +1,32 @@
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
- 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
- import { Type } from "typebox";
4
- import { validateToolArguments } from "@earendil-works/pi-ai";
5
- import { spawn } from "node:child_process";
6
- import * as fs from "node:fs";
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
- import * as os from "node:os";
9
- import { tmpdir } from "node:os";
10
- import * as path from "node:path";
11
- import { dirname, join, resolve } from "node:path";
12
- import { randomUUID } from "node:crypto";
13
- import { MessageChannel, Worker } from "node:worker_threads";
14
- import diagnosticsChannel from "node:diagnostics_channel";
15
- import { fileURLToPath, pathToFileURL } from "node:url";
16
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
- //#region src/runtime/dispatch.ts
18
- /**
19
- * pi.dispatch: the parallel binding that spawns a fresh pi subprocess per call.
20
- *
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.
28
- *
29
- * Behaviourally compatible with pi's examples/extensions/subagent/index.ts reference
30
- * (--mode json, -p, --no-session, --append-system-prompt <tmpfile>), but not cooperative:
31
- * does not require the user to install pi's subagent extension. See ADR-0016 section 2.
32
- */
33
- /**
34
- * The CLI command used to invoke a fresh pi subprocess for one dispatch.
35
- *
36
- * Always `"pi"` — pi-ptc is a library, not a CLI, and `dispatch` must spawn the
37
- * separate `pi` binary regardless of how the host process was launched. An
38
- * earlier copy of this function mirrored pi's own subagent extension and tried
39
- * to reuse `process.execPath + process.argv[1]`, which works only when the host
40
- * is `pi` itself; in any other host (test scripts, the dispatcher host process,
41
- * etc.) the function would spawn the host script in place of `pi`, producing
42
- * infinite recursion and a stdout pipe that never drains (so `proc.on('close')`
43
- * never fires). The caller surfaces a missing-binary condition via the child's
44
- * `error` event — `spawn` itself does not throw synchronously on ENOENT, the
45
- * async `error` listener does.
46
- */
47
- const PI_COMMAND = "pi";
48
- /**
49
- * Compose the system prompt handed to the child subprocess.
50
- *
51
- * The agent's own markdown body is the user-supplied content; pi-ptc appends a
52
- * `<pi-ptc-context depth="N" max-depth="M">...</pi-ptc-context>` hint so the child
53
- * agent can see how much recursion room it has. The shape mirrors DSH's existing
54
- * subagent-context injection so the field recognises it.
55
- */
56
- function appendDepthHint(systemPrompt, depth, maxDepth) {
57
- const hint = [
58
- "<pi-ptc-context depth=\"" + depth + "\" max-depth=\"" + maxDepth + "\">",
59
- "You are a PTC run at depth " + depth + " (root is depth 0). You may write PTC programs and you may",
60
- "call pi.dispatch(...) to spawn further children, but each level costs a fresh pi subprocess.",
61
- "The remaining depth budget is " + maxDepth + " - " + depth + ". Beyond it, pi.dispatch rejects with",
62
- "{ status: \"rejected\", errorMessage: \"dispatch depth limit reached\" }.",
63
- "</pi-ptc-context>"
64
- ].join("\n");
65
- return systemPrompt.length === 0 ? hint : systemPrompt + "\n\n" + hint;
66
- }
67
- /** Result returned when the depth limit is exceeded. */
68
- function dispatchDepthLimitReached() {
69
- return {
70
- text: "",
71
- status: "rejected",
72
- agentName: "unknown",
73
- durationMs: 0,
74
- exitCode: -1,
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
93
- };
94
- }
95
- /**
96
- * Decide the outcome of one `dispatch(...)` based on the child's `close` event.
97
- *
98
- * Cancellation has priority over every other branch: when the host's signal
99
- * aborted the run, the child exited because we asked it to, and the contract
100
- * (ADR-0016 §4) is to surface "dispatch cancelled" rather than leave the
101
- * caller guessing why an aborted run came back `rejected`.
102
- *
103
- * Non-cancellation cases follow the original close-handler ladder:
104
- * - exit 0 with final text → fulfilled
105
- * - exit 0 without final text → rejected (the model never answered)
106
- * - any other exit code → rejected, no errorMessage (spawn-error path labels
107
- * its own failures with `failed to spawn pi: ...`)
108
- */
109
- function decideCloseOutcome(input) {
110
- if (input.aborted) return {
111
- status: "rejected",
112
- errorMessage: "dispatch cancelled"
113
- };
114
- if (input.exitCode === 0 && input.finalText.length > 0) return { status: "fulfilled" };
115
- if (input.exitCode === 0) return {
116
- status: "rejected",
117
- errorMessage: "dispatch produced no final text"
118
- };
119
- return { status: "rejected" };
120
- }
121
- /**
122
- * Send `signal` to `proc` iff the process is still attached, swallowing
123
- * "process already gone" throws. Returns whether a kill was actually issued.
124
- *
125
- * `proc.pid === undefined` happens for processes spawned without a usable
126
- * pid (rare, but TS-strict demands the guard); `proc.kill` throwing happens
127
- * for processes that exited between the guard and the syscall (the OS hands
128
- * back ESRCH). Both are "no-op successfully" for our purposes.
129
- */
130
- function safeKill(proc, signal) {
131
- if (proc.pid === void 0) return false;
132
- try {
133
- proc.kill(signal);
134
- return true;
135
- } catch {
136
- return false;
137
- }
138
- }
139
- /**
140
- * Minimal YAML frontmatter scalar parser. Returns the trimmed value, with surrounding
141
- * double or single quotes stripped when both are present.
142
- */
143
- function extractYamlString(yaml, key) {
144
- const m = yaml.match(new RegExp("^" + key + ":\\s*(.+)$", "m"));
145
- if (!m) return void 0;
146
- let v = (m[1] ?? "").trim();
147
- if (v.length >= 2) {
148
- const first = v[0];
149
- const last = v[v.length - 1];
150
- if (first === "\"" && last === "\"" || first === "'" && last === "'") v = v.slice(1, -1);
151
- }
152
- return v.length === 0 ? void 0 : v;
153
- }
154
- /**
155
- * Minimal YAML list parser: accepts inline `[a, b, c]` and bare `a, b, c`.
156
- */
157
- function extractYamlList(yaml, key) {
158
- const m = yaml.match(new RegExp("^" + key + ":\\s*(.+)$", "m"));
159
- if (!m) return void 0;
160
- let inner = (m[1] ?? "").trim();
161
- if (inner.startsWith("[") && inner.endsWith("]")) inner = inner.slice(1, -1);
162
- const parts = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
163
- return parts.length === 0 ? void 0 : parts;
164
- }
165
- /**
166
- * Parse an agent markdown file: YAML frontmatter (name / description / model / tools)
167
- * plus the body, returned as the system prompt. Tolerates a missing or partial
168
- * frontmatter; the caller falls back to defaults when this returns null.
169
- */
170
- function parseAgentMarkdown(content) {
171
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
172
- if (!fmMatch) return null;
173
- const frontmatter = fmMatch[1] ?? "";
174
- const body = fmMatch[2] ?? "";
175
- const name = extractYamlString(frontmatter, "name");
176
- if (!name) return null;
177
- const description = extractYamlString(frontmatter, "description") ?? "";
178
- const model = extractYamlString(frontmatter, "model") ?? "";
179
- const tools = extractYamlList(frontmatter, "tools");
180
- const out = {
181
- name,
182
- systemPrompt: body.trim()
183
- };
184
- if (description !== void 0) out.description = description;
185
- if (model !== void 0) out.model = model;
186
- if (tools !== void 0) out.tools = tools;
187
- return out;
188
- }
189
- /**
190
- * Discover an agent by name. Mirrors pi's subagent extension agentScope behaviour:
191
- * user-level (`~/.pi/agent/agents/`) is loaded by default; project-level
192
- * (`<cwd>/.pi/agents/`) is loaded only when agentScope includes it.
193
- */
194
- function discoverAgent(name, cwd, agentScope) {
195
- const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
196
- const projectDir = path.join(cwd, ".pi", "agents");
197
- const candidates = [];
198
- if (agentScope === "user" || agentScope === "both") candidates.push({
199
- path: path.join(userDir, name + ".md"),
200
- source: "user"
201
- });
202
- if (agentScope === "project" || agentScope === "both") candidates.push({
203
- path: path.join(projectDir, name + ".md"),
204
- source: "project"
205
- });
206
- for (const c of candidates) {
207
- let raw;
208
- try {
209
- raw = fs.readFileSync(c.path, "utf-8");
210
- } catch {
211
- continue;
212
- }
213
- const parsed = parseAgentMarkdown(raw);
214
- if (parsed && parsed.name === name) {
215
- const out = {
216
- name: parsed.name,
217
- source: c.source,
218
- systemPrompt: parsed.systemPrompt
219
- };
220
- if (parsed.model !== void 0) out.model = parsed.model;
221
- if (parsed.tools !== void 0) out.tools = parsed.tools;
222
- return out;
223
- }
224
- }
225
- return null;
226
- }
227
- /**
228
- * Build the argv handed to the child pi subprocess. Model / thinking / tools / agentScope
229
- * precedence: dispatch input (call site) > agent markdown frontmatter > nothing.
230
- * The agent's system prompt (with the depth hint appended) is materialised to a tmpfile
231
- * and passed via --append-system-prompt, exactly as the subagent extension does.
232
- */
233
- function buildArgv(input, agent, promptFilePath) {
234
- const args = [
235
- "--mode",
236
- "json",
237
- "-p",
238
- "--no-session"
239
- ];
240
- const model = input.model ?? agent.model;
241
- if (model) args.push("--model", model);
242
- const thinking = input.thinkingLevel ?? "off";
243
- args.push("--thinking", thinking);
244
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
245
- args.push("--append-system-prompt", promptFilePath);
246
- args.push("Task: " + input.task);
247
- return args;
248
- }
249
- function parseAgentEvent(line) {
250
- const trimmed = line.trim();
251
- if (trimmed.length === 0) return null;
252
- try {
253
- return JSON.parse(trimmed);
254
- } catch {
255
- return null;
256
- }
257
- }
258
- /**
259
- * Write the system prompt (with depth hint appended) to a tmpfile. The caller is
260
- * responsible for cleanup; we use mkdtemp + 0o600 to keep the prompt private, then
261
- * best-effort cleanup in the dispatch() Promise.
262
- */
263
- async function writePromptToTempFile(agentName, prompt) {
264
- const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-dispatch-"));
265
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
266
- const filePath = path.join(tmpDir, "prompt-" + safeName + ".md");
267
- await fs.promises.writeFile(filePath, prompt, {
268
- encoding: "utf-8",
269
- mode: 384
270
- });
271
- return {
272
- dir: tmpDir,
273
- filePath
274
- };
275
- }
276
- /**
277
- * Best-effort cleanup of the tmpfile + dir used for the system prompt. Errors are
278
- * deliberately swallowed: leaving a tmpfile around is preferable to crashing the run.
279
- */
280
- function cleanupTmp(tmp) {
281
- try {
282
- fs.unlinkSync(tmp.filePath);
283
- } catch {}
284
- try {
285
- fs.rmdirSync(tmp.dir);
286
- } catch {}
287
- }
288
- /**
289
- * Spawn one pi subprocess for one dispatch and resolve with a DispatchResult.
290
- *
291
- * Behavioural contract (ADR-0016 sections 1, 3, 4):
292
- * - Resolves with status: fulfilled when the child exits 0 and emits at least one
293
- * assistant message whose text is non-empty.
294
- * - Resolves with status: rejected on non-zero exit, signal kill, spawn failure,
295
- * unknown agent, or no usable final text.
296
- * - Never rejects. The caller can pattern-match on outcome.status the same way
297
- * it would pattern-match a Promise.allSettled record.
298
- * - Honours signal with SIGTERM, then SIGKILL after a 5s grace window.
299
- */
300
- async function dispatch(input, ctx) {
301
- const childDepth = ctx.depth + 1;
302
- if (childDepth > ctx.maxDispatchDepth) return dispatchDepthLimitReached();
303
- const cwd = input.cwd ?? ctx.cwd;
304
- const agentScope = input.agentScope ?? "user";
305
- const start = Date.now();
306
- const agent = discoverAgent(input.agent, cwd, agentScope);
307
- if (!agent) return {
308
- text: "",
309
- status: "rejected",
310
- agentName: input.agent,
311
- durationMs: Date.now() - start,
312
- exitCode: 1,
313
- errorMessage: "unknown agent: " + input.agent + " (agentScope=" + agentScope + ", cwd=" + cwd + ")"
314
- };
315
- const fullPrompt = appendDepthHint(agent.systemPrompt, childDepth, ctx.maxDispatchDepth);
316
- const tmp = await writePromptToTempFile(agent.name, fullPrompt);
317
- const argv = buildArgv(input, agent, tmp.filePath);
318
- return await new Promise((resolve) => {
319
- let stdoutBuffer = "";
320
- let stderrBuf = "";
321
- let finalText = "";
322
- let usage;
323
- let exitCode = -1;
324
- let resolved = false;
325
- let killTimer;
326
- let proc;
327
- let aborted = false;
328
- const finalize = (status, errorMessage) => {
329
- if (resolved) return;
330
- resolved = true;
331
- if (killTimer) clearTimeout(killTimer);
332
- if (ctx.signal) ctx.signal.removeEventListener("abort", onAbort);
333
- cleanupTmp(tmp);
334
- const out = {
335
- text: finalText,
336
- status,
337
- agentName: agent.name,
338
- durationMs: Date.now() - start,
339
- exitCode
340
- };
341
- if (usage) out.usage = usage;
342
- if (stderrBuf.length > 0) out.stderr = stderrBuf;
343
- if (errorMessage) out.errorMessage = errorMessage;
344
- resolve(out);
345
- };
346
- const onAbort = () => {
347
- if (!proc || proc.killed || resolved) return;
348
- aborted = true;
349
- safeKill(proc, "SIGTERM");
350
- killTimer = setTimeout(() => {
351
- if (proc && !proc.killed) safeKill(proc, "SIGKILL");
352
- }, 5e3);
353
- };
354
- try {
355
- proc = spawn(PI_COMMAND, argv, {
356
- cwd,
357
- shell: false,
358
- stdio: [
359
- "ignore",
360
- "pipe",
361
- "pipe"
362
- ],
363
- env: {
364
- ...process.env,
365
- PI_PTC_DEPTH: String(childDepth)
366
- }
367
- });
368
- } catch (err) {
369
- finalize("rejected", "failed to spawn pi: " + (err instanceof Error ? err.message : String(err)));
370
- return;
371
- }
372
- if (ctx.signal) {
373
- if (ctx.signal.aborted) onAbort();
374
- else ctx.signal.addEventListener("abort", onAbort, { once: true });
375
- }
376
- const stdout = proc.stdout;
377
- if (!stdout) return;
378
- stdout.on("data", (data) => {
379
- stdoutBuffer += data.toString("utf-8");
380
- let nl = stdoutBuffer.indexOf("\n");
381
- while (nl >= 0) {
382
- const line = stdoutBuffer.slice(0, nl);
383
- stdoutBuffer = stdoutBuffer.slice(nl + 1);
384
- const ev = parseAgentEvent(line);
385
- if (!ev) {
386
- nl = stdoutBuffer.indexOf("\n");
387
- continue;
388
- }
389
- if (ev.type === "message_end" && ev.message && ev.message.role === "assistant") {
390
- const m = ev.message;
391
- if (m.usage) {
392
- const cur = usage ?? {
393
- input: 0,
394
- output: 0,
395
- cacheRead: 0,
396
- cacheWrite: 0,
397
- cost: 0,
398
- turns: 0
399
- };
400
- cur.input += m.usage.input ?? 0;
401
- cur.output += m.usage.output ?? 0;
402
- cur.cacheRead += m.usage.cacheRead ?? 0;
403
- cur.cacheWrite += m.usage.cacheWrite ?? 0;
404
- cur.cost += m.usage.cost?.total ?? 0;
405
- cur.turns += 1;
406
- usage = cur;
407
- }
408
- if (Array.isArray(m.content)) {
409
- for (const part of m.content) if (part && part.type === "text" && typeof part.text === "string") finalText = part.text;
410
- }
411
- }
412
- nl = stdoutBuffer.indexOf("\n");
413
- }
414
- });
415
- const stderrStream = proc.stderr;
416
- if (!stderrStream) return;
417
- stderrStream.on("data", (data) => {
418
- stderrBuf += data.toString("utf-8");
419
- });
420
- proc.on("close", (code) => {
421
- exitCode = code ?? -1;
422
- const { status, errorMessage } = decideCloseOutcome({
423
- exitCode,
424
- finalText,
425
- aborted
426
- });
427
- finalize(status, errorMessage);
428
- });
429
- proc.on("error", (err) => {
430
- stderrBuf += "[spawn-error] " + err.message + "\n";
431
- finalize("rejected", "failed to spawn pi: " + err.message);
432
- });
433
- });
434
- }
435
- //#endregion
436
- //#region src/runtime/bindings.ts
437
- /**
438
- * Bindings: the pi tools a PTC program may call as `tools.<name>(args)`.
439
- *
440
- * R5 found that pi's extension API exposes no tool-dispatch surface — `pi.on('tool_call')`
441
- * only fires from the agent loop, `extensionsResult.runtime` is state plumbing, and
442
- * `pi.exec` has no `execArgv`/`resourceLimits`/`stdio`. So a binding builds the tool with
443
- * pi's own SDK factory and calls `execute()` on it directly. That keeps the tool
444
- * implementation identical to the native one, but it also means worker-driven calls are
445
- * **not** seen by pi's tool-call pipeline: `tool_call` guards, `protected-paths`-style
446
- * extensions, permission gates and sandbox overrides never run for them. This is the
447
- * inherited risk ADR-0005 accepts and records — it must not be described as resolved, and
448
- * nothing here may pretend to route through `pi.on('tool_call')`.
449
- *
450
- * Arguments are validated with pi's own `validateToolArguments`, the same helper the agent
451
- * loop uses, so a bad call fails with the same message (and the same coercion) it would
452
- * get from the native pipeline.
453
- */
454
- /** pi's built-in tools that can be exposed as bindings, in native order. */
455
- const BUILTIN_BINDING_NAMES = [
456
- "read",
457
- "bash",
458
- "edit",
459
- "write",
460
- "grep",
461
- "find",
462
- "ls"
463
- ];
464
- /** Parallel binding name (ADR-0016). */
465
- const DISPATCH_BINDING_NAME = "pi.dispatch";
466
- /**
467
- * Bindings exposed when the caller does not pass an explicit name list.
468
- *
469
- * `bash` is included on purpose: DSH's PTC preset still mounts `tool-bash`/`tool-pwsh`
470
- * (R1 §2 — PTC mode hides the tools from the wire list and re-exposes the same registry
471
- * as bindings), so a PTC program written against DSH may run shell commands. Leaving it
472
- * out would silently shrink the surface relative to DSH. Callers that want a read-only
473
- * PTC surface pass an explicit subset.
474
- */
475
- const DEFAULT_BINDING_NAMES = BUILTIN_BINDING_NAMES;
476
- const BUILTIN_TOOL_FACTORIES = {
477
- read: createReadTool,
478
- bash: createBashTool,
479
- edit: createEditTool,
480
- write: createWriteTool,
481
- grep: createGrepTool,
482
- find: createFindTool,
483
- ls: createLsTool
484
- };
485
- /**
486
- * Build the binding table for one run.
487
- *
488
- * Tools are created once per table and reused across calls, matching pi's own extension
489
- * examples (`createXxxTool(cwd)` caches nothing internally, so one instance per run is
490
- * the intended granularity).
491
- */
492
- function createBuiltinBindings(options) {
493
- const names = options.names ?? DEFAULT_BINDING_NAMES;
494
- const includeDispatch = options.includeDispatch ?? options.names === void 0;
495
- const table = /* @__PURE__ */ new Map();
496
- for (const name of names) {
497
- const factory = BUILTIN_TOOL_FACTORIES[name];
498
- if (!factory) throw new TypeError(`unknown PTC binding "${name}"; known bindings: ${BUILTIN_BINDING_NAMES.join(", ")}`);
499
- const tool = factory(options.cwd);
500
- table.set(name, {
501
- name,
502
- execute: async (args, context) => {
503
- const toolCallId = `ptc:${context.callId}`;
504
- const toolCall = {
505
- type: "toolCall",
506
- id: toolCallId,
507
- name: tool.name,
508
- arguments: args
509
- };
510
- const validated = validateToolArguments(tool, toolCall);
511
- const result = await tool.execute(toolCallId, validated, context.signal, void 0);
512
- return {
513
- content: result.content,
514
- details: result.details === void 0 ? null : result.details
515
- };
516
- }
517
- });
518
- }
519
- if (includeDispatch) table.set(DISPATCH_BINDING_NAME, {
520
- name: DISPATCH_BINDING_NAME,
521
- execute: async (args, context) => {
522
- return dispatch(args, {
523
- signal: context.signal,
524
- callId: context.callId,
525
- cwd: options.cwd,
526
- depth: context.depth,
527
- maxDispatchDepth: context.maxDispatchDepth
528
- });
529
- }
530
- });
531
- return table;
532
- }
533
- //#endregion
534
- //#region src/runtime/limits.ts
535
- const DEFAULT_CONFIG = Object.freeze({
536
- timeoutMs: 12e4,
537
- maxTimeoutMs: 6e5,
538
- maxOutputBytes: 67108864,
539
- maxMessageBytes: 134217728,
540
- maxPendingCalls: 128,
541
- maxParallelSubCalls: 10,
542
- dispatchConcurrency: 8,
543
- maxDispatchDepth: 3,
544
- maxItemsPerCall: 4096,
545
- graceMs: 3e3,
546
- maxOldGenerationSizeMb: 512,
547
- maxYoungGenerationSizeMb: 64,
548
- poolSize: 4,
549
- poolAcquireTimeoutMs: 3e4,
550
- drainGraceMs: 5e3
551
- });
552
- /**
553
- * F1 — the only environment variables a PTC worker may inherit.
554
- *
555
- * These are what a shell needs to resolve binaries on each platform; everything
556
- * else the host has (tokens, API keys, proxies, home paths) stays out of the worker.
557
- */
558
- const WORKER_ENV_ALLOW_LIST = Object.freeze([
559
- "PATH",
560
- "PATHEXT",
561
- "SYSTEMROOT",
562
- "WINDIR",
563
- "TEMP",
564
- "TMP"
565
- ]);
566
- /**
567
- * Build the per-run environment snapshot: the allow-list entries that actually exist.
568
- *
569
- * Unset or empty entries are dropped rather than passed through as empty strings, so
570
- * `Object.keys(workerEnv)` is exactly what the worker sees in `process.env`.
571
- */
572
- function createWorkerEnv(source = process.env) {
573
- const env = {};
574
- for (const name of WORKER_ENV_ALLOW_LIST) {
575
- const value = source[name];
576
- if (typeof value === "string" && value.length > 0) env[name] = value;
577
- }
578
- return env;
579
- }
580
- /**
581
- * Merge per-run overrides onto `DEFAULT_CONFIG`.
582
- *
583
- * Overrides are validated rather than coerced: a negative or non-numeric limit is a
584
- * caller bug, and silently substituting a default would hide it.
585
- */
586
- function resolveConfig(overrides = {}) {
587
- const resolved = { ...DEFAULT_CONFIG };
588
- for (const key of Object.keys(DEFAULT_CONFIG)) {
589
- const override = overrides[key];
590
- if (override === void 0) continue;
591
- if (typeof override !== "number" || !Number.isFinite(override) || override <= 0) throw new TypeError(`invalid PTC config override for ${key}: ${String(override)}`);
592
- resolved[key] = override;
593
- }
594
- return resolved;
595
- }
596
- /**
597
- * Resolve the effective deadline for one run.
598
- *
599
- * DSH semantics (R1 §3): `0` does not disable the deadline, it falls back to the
600
- * default; anything larger is clamped to `maxTimeoutMs`.
601
- */
602
- function effectiveTimeoutMs(requested, config = DEFAULT_CONFIG) {
603
- if (requested === void 0 || !Number.isFinite(requested) || requested <= 0) return config.timeoutMs;
604
- return Math.min(requested, config.maxTimeoutMs);
605
- }
606
- //#endregion
607
- //#region src/runtime/worker-pool.ts
608
- /**
609
- * Per-turn worker pool: keeps `worker_threads` Workers warm across PTC runs so the
610
- * second `ptc_run_code` of a turn does not pay the V8 spin-up + module-graph load cost
611
- * the first one did (ADR-0017).
612
- *
613
- * The pool is created by the pi extension hook at turn entry, passed to
614
- * `runPtcProgram()` via `RunPtcProgramOptions.pool`, and drained at turn exit. Two
615
- * surfaces (`run_code`, `workflow`) do not share a pool: their worker programs are
616
- * different (`installWorkflowHelpers` in `worker-main.ts`), and one pool per surface
617
- * is the cleanest ownership story.
618
- *
619
- * Capacity model
620
- * ──────────────
621
- * "Resident workers" ≤ `size`, "in-flight calls" ≤ unbounded but queued when the pool
622
- * is full. An acquire when the pool is at capacity joins a FIFO wait list; a waiter is
623
- * served the instant an `acquire()` resolves with the released worker — the released
624
- * worker is handed directly to the next waiter rather than going through `idle`. This
625
- * matches the host's own `acquireDispatchSlot` semantics: the queue is the throughput
626
- * story, not the rejection story.
627
- *
628
- * Waiters time out after `acquireTimeoutMs`; on timeout the run fails with
629
- * `kind: workerExit` and a message naming the pool (ADR-0017 W-2 / §4).
630
- *
631
- * `drain()` — awaited by the pi extension hook at turn end — gives in-flight runs
632
- * `drainGraceMs` (default 5 000 ms) to release their worker. Whatever is still in flight
633
- * when the grace expires is terminated together with the idle workers: a worker whose
634
- * `release()` never arrives (a stuck dispatcher promise, a crash no one observed) must
635
- * bound the turn-end hook, not hang it. Drain therefore always resolves — expiry is not
636
- * an error, because a turn boundary should not fail over one bad worker.
637
- *
638
- * Health
639
- * ──────
640
- * A worker that has emitted `exit` is retired — it never re-enters `idle`. The pool
641
- * detects exit on the next `release()`: if `worker.threadId === -1` (Node marks a
642
- * terminated worker with a sentinel threadId) the worker is dropped rather than kept
643
- * idle. This keeps a worker that crashed between two runs from being handed out again.
644
- */
645
- /**
646
- * Build the `Worker` options for one spawn (ADR-0005 F1–F3 hardening in one place).
647
- *
648
- * `runId` is optional and deliberately absent on the pooled path: a warm worker serves many runs,
649
- * so its spawn-time `workerData` cannot carry any single run's identity (each run's `runId`
650
- * travels in its `init` frame instead).
651
- */
652
- function workerSpawnOptions(options) {
653
- return {
654
- name: `ptc-${options.surface}`,
655
- env: options.env,
656
- workerData: options.runId === void 0 ? { env: options.env } : {
657
- runId: options.runId,
658
- env: options.env
659
- },
660
- resourceLimits: {
661
- maxOldGenerationSizeMb: options.limits.maxOldGenerationSizeMb,
662
- maxYoungGenerationSizeMb: options.limits.maxYoungGenerationSizeMb
663
- }
664
- };
665
- }
666
- /**
667
- * Diagnostics channels the pool publishes to (ADR-0017 W-5, Addendum). Channels have
668
- * zero subscribers by default — `diagnostics_channel.channel(...)` always returns the
669
- * same instance, but `publish` short-circuits when no one is listening, so this is the
670
- * documented cheap path.
671
- */
672
- const channels = {
673
- acquireLatency: diagnosticsChannel.channel("ptc:pool:acquire-latency"),
674
- resetTime: diagnosticsChannel.channel("ptc:worker:reset-time"),
675
- imageBytes: diagnosticsChannel.channel("ptc:image:hoist-bytes")
676
- };
677
- function publish(channel, message) {
678
- channel.publish(message);
679
- }
680
- /**
681
- * A fixed-capacity FIFO-served worker pool. Not safe for concurrent `acquire()` calls
682
- * beyond Node's microtask interleaving guarantees — callers (`runPtcProgram`) await
683
- * `acquire` before scheduling anything that touches the pool, so the only concurrent
684
- * surface is `release()` against an in-flight worker.
685
- */
686
- var WorkerPool = class {
687
- #buildWorkerUrl;
688
- #size;
689
- #acquireTimeoutMs;
690
- #drainGraceMs;
691
- #workerOptions;
692
- #idle = [];
693
- #inFlight = /* @__PURE__ */ new Set();
694
- #waiters = [];
695
- /**
696
- * When each worker was last released (i.e., the previous run settled). Used by the
697
- * dispatcher to publish `ptc:worker:reset-time` when a warm worker emits its first
698
- * `ready` frame — the time between settle and reset-complete is the worker's
699
- * per-run reset cost (ADR-0017 Addendum).
700
- */
701
- #lastSettledAt = /* @__PURE__ */ new WeakMap();
702
- #totalAcquires = 0;
703
- #poolExhaustions = 0;
704
- /**
705
- * `true` once `drain()` has resolved. After this point every `acquire()` rejects
706
- * — the pool is single-use, the parent turn owns it for one cycle (ADR-0017 §1).
707
- */
708
- #drained = false;
709
- constructor(options) {
710
- if (typeof options.buildWorkerUrl !== "function") throw new TypeError("WorkerPool: buildWorkerUrl must be a function");
711
- if (options.size !== void 0 && (typeof options.size !== "number" || !Number.isFinite(options.size) || options.size <= 0)) throw new TypeError(`WorkerPool: size must be a positive finite number, received ${String(options.size)}`);
712
- if (options.acquireTimeoutMs !== void 0 && (typeof options.acquireTimeoutMs !== "number" || !Number.isFinite(options.acquireTimeoutMs) || options.acquireTimeoutMs <= 0)) throw new TypeError(`WorkerPool: acquireTimeoutMs must be a positive finite number, received ${String(options.acquireTimeoutMs)}`);
713
- if (options.drainGraceMs !== void 0 && (typeof options.drainGraceMs !== "number" || !Number.isFinite(options.drainGraceMs) || options.drainGraceMs <= 0)) throw new TypeError(`WorkerPool: drainGraceMs must be a positive finite number, received ${String(options.drainGraceMs)}`);
714
- this.#buildWorkerUrl = options.buildWorkerUrl;
715
- this.#size = options.size ?? 4;
716
- this.#acquireTimeoutMs = options.acquireTimeoutMs ?? 3e4;
717
- this.#drainGraceMs = options.drainGraceMs ?? 5e3;
718
- this.#workerOptions = options.workerOptions;
719
- }
720
- /**
721
- * Acquire an idle worker, spawn a new one if below capacity, or queue until capacity
722
- * opens up. Throws `Error("pool acquire timed out after ${acquireTimeoutMs} ms")` on
723
- * timeout; the dispatcher wraps that message with a `pool acquire failed: ` prefix (ADR-0017 §4).
724
- */
725
- async acquire() {
726
- if (this.#drained) throw new Error("pool has been drained and can no longer serve workers");
727
- this.#totalAcquires += 1;
728
- const startedAt = Date.now();
729
- let idle = this.#idle.shift();
730
- while (idle !== void 0 && !this.#isHealthy(idle)) idle = this.#idle.shift();
731
- if (idle !== void 0) {
732
- this.#inFlight.add(idle);
733
- idle.ref();
734
- publish(channels.acquireLatency, {
735
- poolSize: this.#size,
736
- waiters: this.#waiters.length,
737
- durationMs: Date.now() - startedAt
738
- });
739
- return idle;
740
- }
741
- if (this.#inFlight.size < this.#size) {
742
- const worker = this.#spawn();
743
- this.#inFlight.add(worker);
744
- publish(channels.acquireLatency, {
745
- poolSize: this.#size,
746
- waiters: this.#waiters.length,
747
- durationMs: Date.now() - startedAt
748
- });
749
- return worker;
750
- }
751
- this.#poolExhaustions += 1;
752
- return await new Promise((resolve, reject) => {
753
- const timer = setTimeout(() => {
754
- const index = this.#waiters.findIndex((entry) => entry.timer === timer);
755
- if (index >= 0) this.#waiters.splice(index, 1);
756
- reject(/* @__PURE__ */ new Error(`pool acquire timed out after ${this.#acquireTimeoutMs} ms`));
757
- }, this.#acquireTimeoutMs);
758
- this.#waiters.push({
759
- resolve,
760
- reject,
761
- timer,
762
- startedAt
763
- });
764
- });
765
- }
766
- /**
767
- * Hand a worker back to the pool. If a waiter is queued, hand the worker directly to
768
- * them (no idle round-trip). Otherwise park the worker in `idle` — unless it has
769
- * already exited, in which case retire it.
770
- */
771
- release(worker) {
772
- if (!this.#inFlight.delete(worker)) return;
773
- if (!this.#isHealthy(worker)) return;
774
- this.#lastSettledAt.set(worker, Date.now());
775
- const waiter = this.#waiters.shift();
776
- if (waiter) {
777
- clearTimeout(waiter.timer);
778
- this.#inFlight.add(worker);
779
- publish(channels.acquireLatency, {
780
- poolSize: this.#size,
781
- waiters: this.#waiters.length,
782
- durationMs: Date.now() - waiter.startedAt
783
- });
784
- waiter.resolve(worker);
785
- return;
786
- }
787
- this.#idle.push(worker);
788
- worker.unref();
789
- }
790
- /**
791
- * Last time `release()` was called for `worker`. The dispatcher reads this to
792
- * publish `ptc:worker:reset-time` on a warm `ready` frame: `now - lastSettledAt`
793
- * is the worker's per-run reset cost (ADR-0017 Addendum). `undefined` for a worker
794
- * the pool never released (cold-start).
795
- */
796
- lastSettledAt(worker) {
797
- return this.#lastSettledAt.get(worker);
798
- }
799
- /**
800
- * Wait for every in-flight worker to be released — bounded by `drainGraceMs` — then
801
- * terminate every resident worker and reject every queued waiter. After `drain()`
802
- * returns the pool is unusable.
803
- *
804
- * The bound matters: `drain()` is awaited from the pi `turn_end` hook, so a worker
805
- * whose `release()` never arrives (a stuck dispatcher promise, an unobserved crash)
806
- * must not hold the turn open. Workers still in flight when the grace expires are
807
- * terminated with the idle ones, and `drain()` resolves normally — a single bad worker
808
- * is not a reason to fail the turn boundary.
809
- */
810
- async drain() {
811
- while (this.#waiters.length > 0) {
812
- const waiter = this.#waiters.shift();
813
- if (!waiter) break;
814
- clearTimeout(waiter.timer);
815
- waiter.reject(/* @__PURE__ */ new Error("pool drained before acquire could be satisfied"));
816
- }
817
- const deadline = Date.now() + this.#drainGraceMs;
818
- while (this.#inFlight.size > 0 && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 5));
819
- const terminating = [...this.#idle, ...this.#inFlight];
820
- this.#idle.length = 0;
821
- this.#inFlight.clear();
822
- await Promise.all(terminating.map((worker) => worker.terminate().catch(() => void 0)));
823
- this.#drained = true;
824
- }
825
- /** Read-only snapshot of the pool's counters; useful for tests and diagnostics. */
826
- stats() {
827
- return {
828
- resident: this.#inFlight.size + this.#idle.length,
829
- inFlight: this.#inFlight.size,
830
- waiters: this.#waiters.length,
831
- totalAcquires: this.#totalAcquires,
832
- poolExhaustions: this.#poolExhaustions
833
- };
834
- }
835
- /**
836
- * Spawn one worker and wire its retirement listener. Private: the pool owns worker
837
- * lifecycle, so a caller outside `acquire()` has no business growing the pool (the
838
- * dispatcher spawns directly itself when no pool is configured).
839
- */
840
- #spawn() {
841
- const worker = new Worker(this.#buildWorkerUrl(), this.#workerOptions);
842
- worker.once("exit", () => {
843
- const idleIndex = this.#idle.indexOf(worker);
844
- if (idleIndex >= 0) {
845
- this.#idle.splice(idleIndex, 1);
846
- return;
847
- }
848
- this.#inFlight.delete(worker);
849
- });
850
- return worker;
851
- }
852
- /**
853
- * A worker is healthy when it has not exited. Node marks a terminated worker with
854
- * `threadId === -1`; the `'exit'` event path also clears it from our lists, but the
855
- * `threadId` check is the cheap synchronous check for the common release path.
856
- *
857
- * Known window: `threadId` flips to `-1` **asynchronously**, so a worker whose
858
- * `terminate()` has been called but whose exit has not been processed yet still reads as
859
- * healthy (measured). That is unreachable in-tree today — `terminate()` is only called by
860
- * `drain()` (after which the pool refuses every `acquire`) and by the dispatcher's
861
- * unpooled path (whose worker never enters this pool) — so this is a note for whoever
862
- * adds a third caller, not a bug to chase.
863
- */
864
- #isHealthy(worker) {
865
- return worker.threadId !== -1;
866
- }
867
- };
868
- /**
869
- * Publish a frame-transfer event when the dispatcher's callResult postMessage carries
870
- * image bytes. Called from the dispatcher, not the pool, but lives here so the channel
871
- * name and payload shape stay one-file-defined (ADR-0017 Addendum).
872
- */
873
- function publishImageBytes(byteLength) {
874
- publish(channels.imageBytes, { byteLength });
875
- }
876
- /**
877
- * Publish the warm-worker's reset cost when its first `ready` frame arrives at the
878
- * host. `previousSettledAt` is the wall-clock time the pool last released this worker;
879
- * the gap is what the worker spent clearing per-run state and re-installing the surface.
880
- */
881
- function publishResetTime(durationMs) {
882
- publish(channels.resetTime, { durationMs });
883
- }
884
- //#endregion
885
- //#region src/runtime/worker-source.ts
886
- /**
887
- * Worker source location.
888
- *
889
- * The previous bootstrap assembled a `data:text/javascript,…` URL from
890
- * `Function.prototype.toString()` of `workerMain` plus a protocol literal —
891
- * a single self-contained file but with no V8 code-cache reuse across spawns
892
- * (ADR-0017 §7). The rolldown dual-entry build now emits `dist/worker.js`
893
- * alongside `dist/index.js`; the host loads that file directly, which lets
894
- * V8's code cache and Node's module cache survive across warm-reuse spawns.
895
- *
896
- * The two candidates the resolver tries:
897
- *
898
- * 1. `dist/worker.js` — the rolldown output, sibling of `dist/index.js`. The
899
- * production path used by `pi install npm:pi-ptc-subagents`.
900
- * 2. `src/runtime/worker-entry.ts` — the source, used by `vitest` runs
901
- * (vitest's transformer handles `.ts` and the worker is launched in-process
902
- * so the relative path resolves).
903
- */
904
- /**
905
- * Resolve the worker entry's `file://` URL.
906
- *
907
- * The function takes no arguments: the protocol tables now travel through the
908
- * worker file itself (rolldown bundles `./protocol.ts` into `dist/worker.js`),
909
- * so the host has nothing to compose at URL-build time.
910
- */
911
- function buildWorkerUrl() {
912
- const here = dirname(fileURLToPath(import.meta.url));
913
- const candidates = [resolve(here, "worker.js"), resolve(here, "worker-entry.ts")];
914
- for (const candidate of candidates) if (existsSync(candidate)) return pathToFileURL(candidate);
915
- throw new Error(`worker entry not found; tried: ${candidates.join(", ")}`);
916
- }
917
- //#endregion
918
- //#region src/runtime/dispatcher.ts
919
- /**
920
- * Host-side dispatcher: one `runPtcProgram()` call = one program in one worker.
921
- *
922
- * The worker's lifecycle depends on the path (ADR-0017 §1):
923
- * - **cold** (no `pool`): the dispatcher spawns a worker for the run and terminates it when the
924
- * run settles — one run, one worker;
925
- * - **pooled** (`pool` set): the dispatcher acquires a warm worker from the turn's pool and
926
- * releases it back when the run settles. The worker outlives the run — the turn's later runs
927
- * reuse it, and the pool's `drain()` retires it at turn end, not this file.
928
- *
929
- * Responsibilities:
930
- * - spawn a hardened worker (F1 env scrub, F2 V8 caps, F3 frozen per-run env in
931
- * `workerData`, F4 `cwd` carried in the run config and handed to the bindings) — the
932
- * hardening itself is defined once, in `workerSpawnOptions` (`worker-pool.ts`),
933
- * - complete the `MessageChannel` handshake and send the `init` frame,
934
- * - route `tools.*` calls to the binding table — concurrently, with DSH's
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.
941
- * - collect logs / narration / phases and enforce the joint output budget,
942
- * - enforce the deadline, honour caller cancellation, and settle the run: hand the worker
943
- * back (pooled) or tear it down (cold).
944
- *
945
- * Everything the worker can influence is treated as untrusted input: every frame goes
946
- * through the protocol guards, is size-checked, and can only ever end the run.
947
- */
948
- function messageOf(error) {
949
- return error instanceof Error ? error.message : String(error);
950
- }
951
- /** A leaf whose bytes JSON cannot express: it is billed by `byteLength`, not by its JSON text. */
952
- function isBinaryLeaf(value) {
953
- return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
954
- }
955
- /**
956
- * Walk a value tree, calling `onBinary` on every binary leaf (`ArrayBuffer` and any
957
- * `ArrayBufferView`). Cycles and shared sub-objects are visited once, so a pathological frame
958
- * cannot wedge or inflate the walk.
959
- *
960
- * Binary leaves are billed here rather than by their JSON text for two reasons: `JSON.stringify`
961
- * serialises an `ArrayBuffer` as `{}` (the size vanishes) and explodes a typed-array view into
962
- * one key per element (the size is overstated by ~6 bytes per element). The walk therefore stops
963
- * at the leaf and never recurses into a view's `.buffer`, which would count the same bytes twice.
964
- * ADR-0017 W-6 spells out the refactor; this is its binary half.
965
- */
966
- function walkBinaryLeaves(value, onBinary) {
967
- const seen = /* @__PURE__ */ new WeakSet();
968
- const visit = (candidate) => {
969
- if (candidate === null || candidate === void 0) return;
970
- if (candidate instanceof ArrayBuffer) {
971
- onBinary(candidate);
972
- return;
973
- }
974
- if (ArrayBuffer.isView(candidate)) {
975
- onBinary(candidate);
976
- return;
977
- }
978
- if (typeof candidate !== "object") return;
979
- if (seen.has(candidate)) return;
980
- seen.add(candidate);
981
- if (Array.isArray(candidate)) {
982
- for (const item of candidate) visit(item);
983
- return;
984
- }
985
- for (const key of Object.keys(candidate)) visit(candidate[key]);
986
- };
987
- visit(value);
988
- }
989
- /**
990
- * Serialized byte size of a frame, the unit both the output and message budgets use.
991
- *
992
- * The count is the JSON text the frame becomes — every brace, bracket, separator, key name and
993
- * scalar — plus the bytes of any binary leaf, which that text cannot express. Two passes:
994
- *
995
- * 1. the whole value through `JSON.stringify` with binary leaves substituted by `null`: every
996
- * structural character, key name and scalar is billed the way the old helper billed it, and a
997
- * cyclic value throws here (as before);
998
- * 2. the binary leaves by `byteLength`, added on top.
999
- *
1000
- * A frame with binary leaves therefore counts 4 bytes (its substituted `null`) more than the JSON
1001
- * text it would have if the bytes vanished. That residue is deliberate and the safe direction:
1002
- * under-counting the payload is the hole ADR-0017 W-6 closes, and the worker's channel clones
1003
- * those bytes for real.
1004
- *
1005
- * A frame with no binary leaf produces exactly the previous helper's number — the substituter is
1006
- * a no-op when there is nothing to substitute — and a cyclic or otherwise unaccountable payload
1007
- * still reports `Number.POSITIVE_INFINITY` so the caller fails the run.
1008
- */
1009
- function serializedBytes(value) {
1010
- try {
1011
- const text = Buffer.byteLength(JSON.stringify(value, (_key, item) => isBinaryLeaf(item) ? null : item) ?? "", "utf8");
1012
- let binary = 0;
1013
- walkBinaryLeaves(value, (leaf) => {
1014
- binary += leaf.byteLength;
1015
- });
1016
- return text + binary;
1017
- } catch {
1018
- return Number.POSITIVE_INFINITY;
1019
- }
1020
- }
1021
- /**
1022
- * Run one PTC program in a fresh worker and resolve with its outcome.
1023
- *
1024
- * Never rejects for program/ limit / cancellation failures — those are reported as
1025
- * `outcome.error` so callers have one place to render from. It only throws on caller bugs
1026
- * (invalid config overrides).
1027
- */
1028
- async function runPtcProgram(options) {
1029
- const config = resolveConfig(options.config);
1030
- const env = createWorkerEnv();
1031
- const runId = options.runId ?? randomUUID();
1032
- const timeoutMs = effectiveTimeoutMs(options.timeoutMs, config);
1033
- const logs = [];
1034
- const narrations = [];
1035
- const phases = [];
1036
- if (options.signal?.aborted) return {
1037
- logs,
1038
- narrations,
1039
- phases,
1040
- error: {
1041
- kind: PTC_ERROR_KIND.abort,
1042
- message: "run cancelled before start"
1043
- }
1044
- };
1045
- let worker;
1046
- try {
1047
- if (options.pool) worker = await options.pool.acquire();
1048
- else worker = new Worker(buildWorkerUrl(), workerSpawnOptions({
1049
- surface: options.surface,
1050
- env,
1051
- limits: config,
1052
- runId
1053
- }));
1054
- } catch (error) {
1055
- return {
1056
- logs,
1057
- narrations,
1058
- phases,
1059
- error: {
1060
- kind: PTC_ERROR_KIND.workerExit,
1061
- message: `pool acquire failed: ${messageOf(error)}`
1062
- }
1063
- };
1064
- }
1065
- return await new Promise((resolve) => {
1066
- const channel = new MessageChannel();
1067
- const control = channel.port1;
1068
- const workerPort = channel.port2;
1069
- const bindingAbort = new AbortController();
1070
- let settled = false;
1071
- /** Set once a stop is under way; owns the terminal state from then on (`settleTerminal`). */
1072
- let cancelling;
1073
- /**
1074
- * Whether the worker has emitted its first `ready` frame. A cancel that lands before it does
1075
- * still arms the grace window (the settle time must be bounded even for a worker that never
1076
- * becomes reachable); the `ready` handler restarts that window, so a merely slow spawn gets
1077
- * its full `graceMs` from the moment it can actually react.
1078
- */
1079
- let workerReady = false;
1080
- let pendingCalls = 0;
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
- */
1088
- let activeDispatches = 0;
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;
1098
- /** Deadline timer: fires `timeoutMs` after the run started. A cancel does not disarm it. */
1099
- let runTimer;
1100
- /** The cancel's cooperative window: armed and re-armed by `armGraceTimer` only. */
1101
- let graceTimer;
1102
- const images = [];
1103
- /**
1104
- * Capture the image blocks from one binding result.
1105
- *
1106
- * `PtcImage` has exactly one representation — base64 `data`, the same shape pi's own `read`
1107
- * returns and the same shape the JSON-only worker channel carries. A binding that emits
1108
- * `data: <base64>` passes through untouched (no decode/encode round trip); a binding that
1109
- * produces raw bytes is normalised here, once, host-side.
1110
- *
1111
- * The caller (`dispatchCall`) only keeps the captured list when `postCallResult` returned
1112
- * true: a frame the port rejected was not a successful subtool result (ADR-0014 §2).
1113
- */
1114
- const captureImages = (value) => {
1115
- const captured = [];
1116
- const content = value?.content;
1117
- if (!Array.isArray(content)) return captured;
1118
- for (const block of content) {
1119
- if (block === null || typeof block !== "object") continue;
1120
- const candidate = block;
1121
- if (candidate.type !== "image") continue;
1122
- let data;
1123
- if (typeof candidate.data === "string" && candidate.data.length > 0) data = candidate.data;
1124
- else if (candidate.bytes instanceof ArrayBuffer && candidate.bytes.byteLength > 0) data = Buffer.from(candidate.bytes).toString("base64");
1125
- if (data === void 0) continue;
1126
- captured.push({
1127
- data,
1128
- mimeType: typeof candidate.mimeType === "string" ? candidate.mimeType : "application/octet-stream"
1129
- });
1130
- }
1131
- return captured;
1132
- };
1133
- const cancelMessage = (reason) => reason === "timeout" ? `run timed out after ${timeoutMs} ms` : "run cancelled";
1134
- /** The error kind a stop reports: whichever reason asked for it. */
1135
- const cancelErrorKind = (reason) => reason === "timeout" ? PTC_ERROR_KIND.timeout : PTC_ERROR_KIND.abort;
1136
- const finish = (outcome) => {
1137
- if (settled) return;
1138
- settled = true;
1139
- if (runTimer) clearTimeout(runTimer);
1140
- if (graceTimer) clearTimeout(graceTimer);
1141
- options.signal?.removeEventListener("abort", onAbortSignal);
1142
- bindingAbort.abort();
1143
- control.close();
1144
- if (options.pool) options.pool.release(worker);
1145
- else worker.terminate();
1146
- resolve(outcome);
1147
- };
1148
- const fail = (kind, message, stack) => {
1149
- finish({
1150
- logs,
1151
- narrations,
1152
- phases,
1153
- error: stack === void 0 ? {
1154
- kind,
1155
- message
1156
- } : {
1157
- kind,
1158
- message,
1159
- stack
1160
- }
1161
- });
1162
- };
1163
- const sendControl = (frame) => {
1164
- if (settled) return;
1165
- if (!isPtcHostFrame(frame)) {
1166
- fail(PTC_ERROR_KIND.protocol, "internal error: malformed host frame");
1167
- return;
1168
- }
1169
- control.postMessage(frame);
1170
- };
1171
- const sendInit = () => {
1172
- const frame = {
1173
- kind: HOST_FRAME_KIND.init,
1174
- runId,
1175
- surface: options.surface,
1176
- code: options.code,
1177
- bindings: [...options.bindings.keys()],
1178
- bindingCandidates: [...BUILTIN_BINDING_NAMES],
1179
- maxItemsPerCall: config.maxItemsPerCall,
1180
- maxPendingCalls: config.maxPendingCalls,
1181
- ...options.args === void 0 ? {} : { args: options.args }
1182
- };
1183
- if (!isPtcHostFrame(frame)) {
1184
- fail(PTC_ERROR_KIND.protocol, "internal error: malformed init frame");
1185
- return;
1186
- }
1187
- const bytes = serializedBytes(frame);
1188
- if (bytes > config.maxMessageBytes) {
1189
- fail(PTC_ERROR_KIND.protocol, `init frame of ${bytes} bytes exceeds maxMessageBytes (${config.maxMessageBytes})`);
1190
- return;
1191
- }
1192
- sendControl(frame);
1193
- };
1194
- const accountOutput = (bytes, source) => {
1195
- outputBytes += bytes;
1196
- if (outputBytes <= config.maxOutputBytes) return true;
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`);
1198
- return false;
1199
- };
1200
- const acquireBuiltinSlot = async () => {
1201
- if (activeBuiltinCalls < config.maxParallelSubCalls) {
1202
- activeBuiltinCalls += 1;
1203
- return;
1204
- }
1205
- await new Promise((slot) => {
1206
- builtinWaiters.push(slot);
1207
- });
1208
- };
1209
- const releaseBuiltinSlot = () => {
1210
- const next = builtinWaiters.shift();
1211
- if (next) {
1212
- next();
1213
- return;
1214
- }
1215
- activeBuiltinCalls -= 1;
1216
- };
1217
- const handleCall = (frame) => {
1218
- pendingCalls += 1;
1219
- if (pendingCalls > config.maxPendingCalls) {
1220
- fail(PTC_ERROR_KIND.protocol, `more than maxPendingCalls (${config.maxPendingCalls}) binding calls in flight`);
1221
- return;
1222
- }
1223
- dispatchCall(frame).finally(() => {
1224
- pendingCalls -= 1;
1225
- });
1226
- };
1227
- const dispatchCall = async (frame) => {
1228
- const binding = options.bindings.get(frame.tool);
1229
- if (!binding) {
1230
- postCallResult({
1231
- kind: HOST_FRAME_KIND.callResult,
1232
- callId: frame.callId,
1233
- tool: frame.tool,
1234
- ok: false,
1235
- message: `no binding named "${frame.tool}" in this run; available bindings: ` + ([...options.bindings.keys()].join(", ") || "(none)")
1236
- });
1237
- return;
1238
- }
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
- }
1258
- }
1259
- try {
1260
- const value = await binding.execute(frame.args, {
1261
- signal: bindingAbort.signal,
1262
- callId: frame.callId,
1263
- depth: runDepth,
1264
- maxDispatchDepth: config.maxDispatchDepth
1265
- });
1266
- const captured = captureImages(value);
1267
- const callFrame = {
1268
- kind: HOST_FRAME_KIND.callResult,
1269
- callId: frame.callId,
1270
- tool: frame.tool,
1271
- ok: true,
1272
- value
1273
- };
1274
- const callFrameBytes = serializedBytes(callFrame);
1275
- if (callFrameBytes > config.maxMessageBytes) {
1276
- fail(PTC_ERROR_KIND.protocol, `callResult frame of ${callFrameBytes} bytes exceeds maxMessageBytes (${config.maxMessageBytes})`);
1277
- return;
1278
- }
1279
- if (postCallResult(callFrame)) for (const image of captured) {
1280
- images.push(image);
1281
- publishImageBytes(Buffer.byteLength(image.data, "base64"));
1282
- }
1283
- } catch (error) {
1284
- postCallResult({
1285
- kind: HOST_FRAME_KIND.callResult,
1286
- callId: frame.callId,
1287
- tool: frame.tool,
1288
- ok: false,
1289
- message: messageOf(error)
1290
- });
1291
- } finally {
1292
- if (isDispatch) activeDispatches -= 1;
1293
- else releaseBuiltinSlot();
1294
- }
1295
- };
1296
- /** `true` when the worker actually received the result (see the hoist at the call site). */
1297
- const postCallResult = (frame) => {
1298
- if (settled) return false;
1299
- try {
1300
- control.postMessage(frame);
1301
- return true;
1302
- } catch (error) {
1303
- try {
1304
- control.postMessage({
1305
- kind: HOST_FRAME_KIND.callResult,
1306
- callId: frame.callId,
1307
- tool: frame.tool,
1308
- ok: false,
1309
- message: `binding result could not be transferred: ${messageOf(error)}`
1310
- });
1311
- } catch {
1312
- fail(PTC_ERROR_KIND.protocol, "binding result is not transferable");
1313
- }
1314
- return false;
1315
- }
1316
- };
1317
- const settleTerminal = (outcome) => {
1318
- if (!cancelling) return outcome;
1319
- return {
1320
- logs,
1321
- narrations,
1322
- phases,
1323
- error: {
1324
- kind: cancelErrorKind(cancelling),
1325
- message: cancelMessage(cancelling)
1326
- }
1327
- };
1328
- };
1329
- const handleFrame = (frame) => {
1330
- if (settled) return;
1331
- if (!isPtcWorkerFrame(frame)) {
1332
- fail(PTC_ERROR_KIND.protocol, "malformed worker frame");
1333
- return;
1334
- }
1335
- const bytes = frame.kind === WORKER_FRAME_KIND.ready ? 0 : serializedBytes(frame);
1336
- if (!Number.isFinite(bytes)) {
1337
- fail(PTC_ERROR_KIND.protocol, "worker frame could not be serialized for accounting");
1338
- return;
1339
- }
1340
- if (bytes > config.maxMessageBytes) {
1341
- fail(PTC_ERROR_KIND.protocol, `control frame of ${bytes} bytes exceeds maxMessageBytes (${config.maxMessageBytes})`);
1342
- return;
1343
- }
1344
- switch (frame.kind) {
1345
- case WORKER_FRAME_KIND.ready:
1346
- if (options.pool) {
1347
- const previousSettled = options.pool.lastSettledAt(worker);
1348
- if (previousSettled !== void 0) publishResetTime(Date.now() - previousSettled);
1349
- }
1350
- if (!workerReady) {
1351
- workerReady = true;
1352
- if (cancelling) {
1353
- armGraceTimer(cancelling);
1354
- return;
1355
- }
1356
- }
1357
- sendInit();
1358
- return;
1359
- case WORKER_FRAME_KIND.call:
1360
- handleCall(frame);
1361
- return;
1362
- case WORKER_FRAME_KIND.log:
1363
- if (accountOutput(bytes, "console output")) logs.push(frame.text);
1364
- return;
1365
- case WORKER_FRAME_KIND.narration:
1366
- if (accountOutput(bytes, "workflow narration")) narrations.push(frame.message);
1367
- return;
1368
- case WORKER_FRAME_KIND.phase:
1369
- if (accountOutput(bytes, "workflow phase")) phases.push(frame.title);
1370
- return;
1371
- case WORKER_FRAME_KIND.result: {
1372
- if (!accountOutput(bytes, "completion value")) return;
1373
- const base = {
1374
- logs,
1375
- narrations,
1376
- phases,
1377
- ...images.length > 0 ? { images } : {}
1378
- };
1379
- finish(settleTerminal(frame.value === void 0 ? base : {
1380
- ...base,
1381
- value: frame.value
1382
- }));
1383
- return;
1384
- }
1385
- case WORKER_FRAME_KIND.error:
1386
- finish(settleTerminal({
1387
- logs,
1388
- narrations,
1389
- phases,
1390
- error: frame.error
1391
- }));
1392
- return;
1393
- default: fail(PTC_ERROR_KIND.protocol, "unknown worker frame");
1394
- }
1395
- };
1396
- function onAbortSignal() {
1397
- beginCancel("abort");
1398
- }
1399
- /**
1400
- * Arm the cooperative-cancel window: `graceMs` from now the run settles with `reason`, unless
1401
- * the worker settles it first. Re-arming replaces the pending window.
1402
- *
1403
- * The invariant this guards: **from the moment a run starts, it settles in bounded time.**
1404
- * The window is therefore armed by `beginCancel` itself, not by the worker's `ready` frame —
1405
- * a worker that never becomes reachable (spawn failure, wedged module load, a thread the OS
1406
- * stopped scheduling) can no longer leave the run pending forever.
1407
- */
1408
- const armGraceTimer = (reason) => {
1409
- if (graceTimer) clearTimeout(graceTimer);
1410
- graceTimer = setTimeout(() => {
1411
- fail(cancelErrorKind(reason), cancelMessage(reason));
1412
- }, config.graceMs);
1413
- };
1414
- function beginCancel(reason) {
1415
- if (settled) return;
1416
- if (cancelling) {
1417
- fail(cancelErrorKind(cancelling), cancelMessage(cancelling));
1418
- return;
1419
- }
1420
- cancelling = reason;
1421
- bindingAbort.abort();
1422
- sendControl({
1423
- kind: HOST_FRAME_KIND.cancel,
1424
- reason
1425
- });
1426
- armGraceTimer(reason);
1427
- }
1428
- worker.on("error", (error) => {
1429
- fail(PTC_ERROR_KIND.workerExit, `worker failed: ${messageOf(error)}`);
1430
- });
1431
- worker.on("exit", (code) => {
1432
- fail(PTC_ERROR_KIND.workerExit, `worker exited with code ${code} before the run completed`);
1433
- });
1434
- control.on("message", handleFrame);
1435
- control.on("messageerror", () => {
1436
- fail(PTC_ERROR_KIND.protocol, "worker sent a frame that could not be deserialized");
1437
- });
1438
- options.signal?.addEventListener("abort", onAbortSignal, { once: true });
1439
- runTimer = setTimeout(() => {
1440
- beginCancel("timeout");
1441
- }, timeoutMs);
1442
- const connectFrame = {
1443
- kind: HOST_FRAME_KIND.connect,
1444
- port: workerPort
1445
- };
1446
- if (!isPtcHostFrame(connectFrame)) fail(PTC_ERROR_KIND.protocol, "internal error: malformed connect frame");
1447
- else worker.postMessage(connectFrame, [workerPort]);
1448
- });
1449
- }
1450
- //#endregion
1451
- //#region src/tools/text.ts
1452
- /** Hard cap for one rendered line; longer lines are truncated with `…`. */
1453
- const MAX_LINE_CHARS = 200;
1454
- /** A value stays on one line while its inline form fits this; otherwise it renders as a block. */
1455
- const INLINE_MAX_CHARS = 100;
1456
- /** Indent applied per nesting level in block form. */
1457
- const INDENT = " ";
1458
- /**
1459
- * ANSI escapes, in the two shapes that matter: OSC (\`ESC ] …\` to BEL, \`ESC \\\\` or C1 ST) and
1460
- * CSI plus friends (introducer, optional intermediates, optional params, final byte).
1461
- *
1462
- * Built through the \`RegExp\` constructor from an escaped source so no control character appears
1463
- * literally — the repo lints via oxlint, whose \`no-control-regex\` rule rejects that.
1464
- */
1465
- const ANSI_PATTERN = new RegExp(["(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))", "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"].join("|"), "g");
1466
- /** Drop ANSI escape sequences (colours, cursor moves, OSC hyperlinks). */
1467
- function stripAnsi(value) {
1468
- if (!value.includes(String.fromCharCode(27)) && !value.includes(String.fromCharCode(155))) return value;
1469
- return value.replace(ANSI_PATTERN, "");
1470
- }
1471
- /**
1472
- * Make arbitrary program output safe to hand the model.
1473
- *
1474
- * Same three steps as pi's built-ins: strip ANSI, remove `\r` (a progress bar's carriage
1475
- * return is not a line break), then drop control characters other than `\n`/`\t` and the
1476
- * Unicode format characters that break width math. Lone surrogates fall out of code-point
1477
- * iteration for free.
1478
- */
1479
- function sanitizeText(value) {
1480
- const stripped = stripAnsi(value).replace(/\r/g, "");
1481
- if (stripped.length === 0) return stripped;
1482
- let out = "";
1483
- for (const char of stripped) {
1484
- const code = char.codePointAt(0);
1485
- if (code === void 0) continue;
1486
- if (code === 10 || code === 9) {
1487
- out += char;
1488
- continue;
1489
- }
1490
- if (code <= 31) continue;
1491
- if (code >= 65529 && code <= 65531) continue;
1492
- out += char;
1493
- }
1494
- return out;
1495
- }
1496
- /** Object keys that read as identifiers in the summary style are left unquoted. */
1497
- const BARE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1498
- function keyText(key) {
1499
- return BARE_KEY.test(key) ? key : JSON.stringify(key);
1500
- }
1501
- /**
1502
- * The inline form of a value, or `undefined` when it cannot be inlined.
1503
- *
1504
- * A string with a real newline is never inlined: keeping its line structure is the whole point
1505
- * (see ADR-0012), and quoting it would turn one readable block back into `\n` escapes.
1506
- */
1507
- function inlineForm(value) {
1508
- if (value === null) return "null";
1509
- if (typeof value === "number" || typeof value === "boolean") return String(value);
1510
- if (typeof value === "string") {
1511
- const sanitized = sanitizeText(value);
1512
- return sanitized.includes("\n") ? void 0 : JSON.stringify(sanitized);
1513
- }
1514
- if (Array.isArray(value)) {
1515
- const items = [];
1516
- for (const item of value) {
1517
- const rendered = inlineForm(item);
1518
- if (rendered === void 0) return void 0;
1519
- items.push(rendered);
1520
- }
1521
- return `[${items.join(", ")}]`;
1522
- }
1523
- const keys = Object.keys(value);
1524
- if (keys.length === 0) return "{}";
1525
- const parts = [];
1526
- for (const key of keys) {
1527
- const rendered = inlineForm(value[key] ?? null);
1528
- if (rendered === void 0) return void 0;
1529
- parts.push(`${keyText(key)}: ${rendered}`);
1530
- }
1531
- return `{${parts.join(", ")}}`;
1532
- }
1533
- /** Inline rendering, but only while it stays short enough to read on one line. */
1534
- function tryInline(value) {
1535
- const rendered = inlineForm(value);
1536
- if (rendered === void 0) return void 0;
1537
- return rendered.length <= INLINE_MAX_CHARS ? rendered : void 0;
1538
- }
1539
- /** A non-container value on its own line (block form only meets these defensively). */
1540
- function scalarText(value) {
1541
- if (value === null) return "null";
1542
- return typeof value === "string" ? sanitizeText(value) : String(value);
1543
- }
1544
- /** One key per line, braces kept, string values raw (real newlines preserved, indented). */
1545
- function blockLines(value, indent) {
1546
- if (value === null || typeof value !== "object") return [`${indent}${scalarText(value)}`];
1547
- if (Array.isArray(value)) {
1548
- if (value.length === 0) return [`${indent}[]`];
1549
- const lines = [`${indent}[`];
1550
- for (const item of value) {
1551
- const inline = tryInline(item);
1552
- if (inline !== void 0) {
1553
- lines.push(`${indent}${INDENT}${inline}`);
1554
- continue;
1555
- }
1556
- lines.push(...valueLines(item, indent + INDENT));
1557
- }
1558
- lines.push(`${indent}]`);
1559
- return lines;
1560
- }
1561
- const keys = Object.keys(value);
1562
- if (keys.length === 0) return [`${indent}{}`];
1563
- const lines = [`${indent}{`];
1564
- const inner = indent + INDENT;
1565
- for (const key of keys) {
1566
- const child = value[key] ?? null;
1567
- const inline = tryInline(child);
1568
- if (inline !== void 0) {
1569
- lines.push(`${inner}${keyText(key)}: ${inline}`);
1570
- continue;
1571
- }
1572
- if (typeof child === "string") {
1573
- lines.push(`${inner}${keyText(key)}:`);
1574
- for (const line of sanitizeText(child).split("\n")) lines.push(line.length === 0 ? "" : `${inner}${INDENT}${line}`);
1575
- continue;
1576
- }
1577
- const nested = blockLines(child, inner);
1578
- lines.push(`${inner}${keyText(key)}: ${(nested[0] ?? "").trimStart()}`.trimEnd(), ...nested.slice(1));
1579
- }
1580
- lines.push(`${indent}}`);
1581
- return lines;
1582
- }
1583
- function valueLines(value, indent) {
1584
- if (typeof value === "string") return sanitizeText(value).split("\n");
1585
- if (value === null || typeof value !== "object") return [String(value)];
1586
- return blockLines(value, indent);
1587
- }
1588
- /** Truncate every line that exceeds {@link MAX_LINE_CHARS}. */
1589
- function capLines(text) {
1590
- return text.split("\n").map((line) => line.length > 200 ? `${line.slice(0, 199)}…` : line).join("\n");
1591
- }
1592
- /**
1593
- * Render a completion value for the model: strings verbatim, everything else as a compact
1594
- * `{key: value}` summary or an indented block — never `JSON.stringify` on the whole value, whose
1595
- * escaping destroys exactly the content a reader needs (newlines, quotes, non-ASCII spacing).
1596
- */
1597
- function renderModelValue(value) {
1598
- if (typeof value === "string") return capLines(sanitizeText(value));
1599
- if (value === null || typeof value !== "object") return String(value);
1600
- const inline = tryInline(value);
1601
- return capLines((inline !== void 0 ? [inline] : valueLines(value, "")).join("\n"));
1602
- }
1603
- //#endregion
1604
- //#region src/tools/common.ts
1605
- /**
1606
- * Shared plumbing for the two model-facing PTC tools (`ptc_run_code`, `ptc_workflow`).
1607
- *
1608
- * Both tools are thin. They resolve the run's working directory, hand the program to
1609
- * `runPtcProgram()` (T3), and turn the dispatcher's outcome into one of pi's two tool-result
1610
- * shapes:
1611
- *
1612
- * - **success** — `content` carries one text block (logs, workflow narration, phases, then the
1613
- * completion value) and `details` carries the same facts structurally for rendering and logs;
1614
- * - **failure** — the tool *throws* (pi's convention: `AgentTool.execute` — "Throw on failure
1615
- * instead of encoding errors in `content`"), with R1 §3's message shape
1616
- * `code run failed (<kind>): <message>` plus a `Captured output:` block.
1617
- *
1618
- * The dispatcher never rejects for program failures — it reports them as `outcome.error` — so
1619
- * "did the run fail" is one check in one place, shared by both tools.
1620
- *
1621
- * The text block is the model's copy, so it is cleaned and laid out for reading (`./text.ts`:
1622
- * ANSI stripped, control characters dropped, strings with real newlines kept multi-line) while
1623
- * `details` stays raw for the TUI. See ADR-0012.
1624
- */
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
- /**
1640
- * Binding names for one run: the built-ins this session actually has enabled (T7, #21).
1641
- *
1642
- * A PTC program must never reach further than the session it runs in — a session started
1643
- * with `--tools read` or `--no-builtin-tools` must not be escapable through `tools.<name>`
1644
- * calls. Names outside the built-in factory set are ignored: only tools this package can
1645
- * build adapters for can be bound.
1646
- */
1647
- function resolveBindingNames(activeToolNames) {
1648
- if (activeToolNames === void 0) return BUILTIN_BINDING_NAMES;
1649
- const active = new Set(activeToolNames);
1650
- return BUILTIN_BINDING_NAMES.filter((name) => active.has(name));
1651
- }
1652
- /**
1653
- * One-line entry for the system prompt's "Available tools" section.
1654
- *
1655
- * Custom tools are omitted from that section unless they set this, which matters most in PTC mode:
1656
- * there the two PTC surfaces are the *only* callable tools, so a prompt that lists no usable tool
1657
- * would contradict the tool declarations. `promptGuidelines` cannot substitute for it — those
1658
- * bullets land in the flat `Guidelines` section, which does not tell the model what it can call.
1659
- */
1660
- const PTC_RUN_CODE_SNIPPET = "Run a TypeScript program that composes pi's tools in one shot (tools.<name>(args)); only its return value and console.log come back";
1661
- const PTC_WORKFLOW_SNIPPET = "Run a structured TypeScript workflow with named phases, narration (log/phase) and structured concurrency (parallel/pipeline)";
1662
- /**
1663
- * Guidelines are appended flat to the `Guidelines` section with no tool-name prefix, so each bullet
1664
- * must name its tool explicitly (pi's docs call this out; "Use this tool when…" is ambiguous).
1665
- * These are the normal-mode hints — in PTC mode the injected briefing supersedes them.
1666
- */
1667
- const PTC_TOOL_GUIDELINES = [
1668
- "Image-bearing tool results inside a program (a `tools.read` on a PNG, say) are attached to you after the run — never return image data as the completion value.",
1669
- "Use ptc_run_code when a task needs several tool calls whose intermediate output you do not need to see — gather it inside the program and return only the final value.",
1670
- "Use ptc_workflow instead of ptc_run_code when the work has named phases and the user benefits from seeing progress narration."
1671
- ];
1672
- /** Tool name per worker surface — the names the model calls and reads in failure messages. */
1673
- const SURFACE_TOOL_NAME = {
1674
- run_code: "ptc_run_code",
1675
- workflow: "ptc_workflow"
1676
- };
1677
- /**
1678
- * Resolve the run's working directory from pi's tool execution context.
1679
- *
1680
- * Investigated against `@earendil-works/pi-coding-agent@0.86.1`: `execute`'s fifth parameter is
1681
- * `ExtensionContext`, and the type *does* expose `cwd: string` — documented there as "Current
1682
- * working directory" (`dist/core/extensions/types.d.ts`), the same value pi's own built-in tools
1683
- * resolve relative paths against (`ToolRenderContext.cwd`, and the extension guide's
1684
- * `resolve(ctx.cwd, params.path)` pattern). So the session cwd comes from pi, and this package
1685
- * never has to guess from `process.cwd()`.
1686
- *
1687
- * The `process.cwd()` fallback stays for hand-built contexts (tests, or a future caller passing a
1688
- * partial object); pi always supplies the field.
1689
- */
1690
- function resolveToolCwd(ctx) {
1691
- const cwd = ctx?.cwd;
1692
- return typeof cwd === "string" && cwd.length > 0 ? cwd : process.cwd();
1693
- }
1694
- /**
1695
- * The captured-output lines R1 §3 appends when a run fails.
1696
- *
1697
- * Phases and narrations are prefixed because they are workflow bookkeeping rather than console
1698
- * output; `ptc_run_code` has neither, so its block is exactly the `logs` join R1 describes.
1699
- */
1700
- function capturedOutputLines(outcome) {
1701
- return [
1702
- ...outcome.phases.map((title) => `[phase] ${sanitizeText(title)}`),
1703
- ...outcome.narrations.map((message) => `[log] ${sanitizeText(message)}`),
1704
- ...outcome.logs.map((line) => sanitizeText(line))
1705
- ];
1706
- }
1707
- /**
1708
- * Build the error a failed run is surfaced with (R1 §3's `CodeRunFailedError` message).
1709
- *
1710
- * The model must be able to fix its program from this message alone: the error kind and message
1711
- * explain what broke, and the captured output shows how far the program got before it did. There
1712
- * is no `sandbox` text and no escalation guidance — ADR-0007 ships no OS sandbox, so the only
1713
- * failure classes are the protocol ones.
1714
- */
1715
- function codeRunFailedError(outcome) {
1716
- const error = outcome.error;
1717
- if (error === void 0) return /* @__PURE__ */ new Error("internal error: a successful PTC run was rendered as a failure");
1718
- const captured = capturedOutputLines(outcome);
1719
- const block = captured.length === 0 ? "" : `\nCaptured output:\n${captured.join("\n")}`;
1720
- const failure = new Error(sanitizeText(`code run failed (${error.kind}): ${error.message}${block}`));
1721
- failure.name = "CodeRunFailedError";
1722
- return failure;
1723
- }
1724
- /**
1725
- * Write the untruncated text block to a temp file and return its path.
1726
- *
1727
- * Mirrors what pi's own `bash` does with a long output (its `fullOutputPath`): the run is the only
1728
- * place the text ever existed, so a file under the OS temp dir is the difference between "the rest is
1729
- * gone" and "the rest is one `tools.read` away".
1730
- */
1731
- function writeFullOutput(text) {
1732
- const filePath = join(tmpdir(), `pi-ptc-output-${randomUUID()}.txt`);
1733
- writeFileSync(filePath, text, "utf8");
1734
- return filePath;
1735
- }
1736
- /**
1737
- * The `[Showing … Full output: path]` footer pi's built-ins use, worded for the same numbers.
1738
- *
1739
- * `truncatedBy` decides the wording: a line cut reports the line range, a byte cut also names the
1740
- * ceiling, and a single oversized last line reports its own size — exactly what `bash` prints.
1741
- */
1742
- function truncationFooter(truncation, fullOutputPath) {
1743
- const startLine = truncation.totalLines - truncation.outputLines + 1;
1744
- const endLine = truncation.totalLines;
1745
- if (truncation.lastLinePartial) return `[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine}. Full output: ${fullOutputPath}]`;
1746
- if (truncation.truncatedBy === "lines") return `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${fullOutputPath}]`;
1747
- return `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${fullOutputPath}]`;
1748
- }
1749
- /**
1750
- * Render one successful outcome as pi's tool result.
1751
- *
1752
- * `content` is ordered the way a reader reconstructs the run: the phase roll-up, then workflow
1753
- * narration, then console output, then the completion value, then warnings. Empty runs get R1's
1754
- * placeholder (`(run_code completed with no output)`) so the model always sees a non-empty text
1755
- * block — a tool result with nothing in it looks like a harness failure.
1756
- *
1757
- * `details` is deliberately the *uncleaned* copy: logs keep their ANSI so the TUI can still
1758
- * colour them, and `result` stays structured for `render.ts`.
1759
- */
1760
- function renderToolResult(input) {
1761
- const { outcome, surface } = input;
1762
- const warnings = [...input.warnings ?? []];
1763
- const images = outcome.images ?? [];
1764
- const parts = [];
1765
- if (outcome.phases.length > 0) parts.push(`Phases: ${outcome.phases.map((title) => sanitizeText(title)).join(" → ")}`);
1766
- if (outcome.narrations.length > 0) parts.push(outcome.narrations.map((message) => sanitizeText(message)).join("\n"));
1767
- if (outcome.logs.length > 0) parts.push(outcome.logs.map((line) => sanitizeText(line)).join("\n"));
1768
- if (outcome.value !== void 0) parts.push(renderModelValue(outcome.value));
1769
- for (const warning of warnings) parts.push(`Warning: ${sanitizeText(warning)}`);
1770
- const built = parts.length > 0 ? parts.join("\n") : `(${SURFACE_TOOL_NAME[surface]} completed with no output)`;
1771
- const truncation = truncateTail(built, {
1772
- maxLines: DEFAULT_MAX_LINES,
1773
- maxBytes: DEFAULT_MAX_BYTES
1774
- });
1775
- let fullOutputPath;
1776
- let text = truncation.content;
1777
- if (truncation.truncated) {
1778
- fullOutputPath = writeFullOutput(built);
1779
- text += `\n\n${truncationFooter(truncation, fullOutputPath)}`;
1780
- }
1781
- return {
1782
- content: [{
1783
- type: "text",
1784
- text
1785
- }, ...images.map((image) => ({
1786
- type: "image",
1787
- data: image.data,
1788
- mimeType: image.mimeType
1789
- }))],
1790
- details: {
1791
- surface,
1792
- logs: [...outcome.logs],
1793
- narrations: [...outcome.narrations],
1794
- phases: [...outcome.phases],
1795
- warnings,
1796
- ...outcome.value === void 0 ? {} : { result: outcome.value },
1797
- durationMs: input.durationMs,
1798
- imageCount: images.length,
1799
- ...fullOutputPath === void 0 ? {} : { fullOutputPath }
1800
- }
1801
- };
1802
- }
1803
- //#endregion
1804
- //#region src/tools/render.ts
1805
- /** Call-row label per surface; `ptc_workflow` gets its own so the two rows stay tellable apart. */
1806
- const TOOL_TITLE = {
1807
- run_code: "PTC",
1808
- workflow: "PTC workflow"
1809
- };
1810
- /**
1811
- * Tree-connector layout. The call row owns column 0–2 (`└─ `); everything below it sits at column 3.
1812
- * Children are siblings of the call's content (the summary line `→ {hint}`), so a child row needs a
1813
- * connector to distinguish itself from the summary — and a continuation prefix to distinguish its
1814
- * own wrapped lines from the child's first line.
1815
- *
1816
- * - Root call row: `└─ PTC …`
1817
- * - Root continuation: ` → {hint}` or ` failed: …` (summary; same column as children)
1818
- * - First / middle child: ` ├─ code …`
1819
- * - First / middle cont.: ` │ …`
1820
- * - Last child: ` └─ image …`
1821
- * - Last cont.: ` …` (no connector — same shape as a wrapped value line)
1822
- *
1823
- * `TREE_INDENT` (3 spaces) is the column children and root continuations share.
1824
- */
1825
- const TREE_ROOT = "└─ ";
1826
- const TREE_ROOT_CONT = " ";
1827
- const TREE_INDENT = " ";
1828
- const TREE_FIRST = "├─ ";
1829
- const TREE_LAST = "└─ ";
1830
- const TREE_CONT_FIRST = "│ ";
1831
- const TREE_CONT_LAST = " ";
1832
- /**
1833
- * Gutter width for child labels. `code` / `out` / `value` / `warn` / `image` are 3–5 chars; `phases`
1834
- * is 6 — pad to 8 so the longest label gets two trailing spaces before its content.
1835
- */
1836
- const LABEL_WIDTH = 8;
1837
- const MAX_CODE_LINES_EXPANDED = 3;
1838
- const MAX_CODE_LINE_CHARS = 120;
1839
- const MAX_LOG_LINES_EXPANDED = 12;
1840
- const MAX_PHASES_EXPANDED = 8;
1841
- const MAX_WARNINGS_EXPANDED = 4;
1842
- const MAX_RESULT_HINT_CHARS = 60;
1843
- const MAX_ERROR_CHARS = 120;
1844
- /**
1845
- * A `Component` that right-aligns one meta segment per line.
1846
- *
1847
- * Stateless: it holds already-styled strings and pads them at render time, so pi can re-render at a
1848
- * new width (terminal resize) without any invalidation bookkeeping.
1849
- */
1850
- var PtcRow = class {
1851
- lines;
1852
- constructor(lines) {
1853
- this.lines = lines;
1854
- }
1855
- /** Part of the `Component` contract; nothing is cached, so nothing has to be dropped. */
1856
- invalidate() {}
1857
- render(width) {
1858
- const safeWidth = Math.max(1, Math.floor(width));
1859
- return this.lines.map((line) => alignRow(line, safeWidth));
1860
- }
1861
- };
1862
- /**
1863
- * Lay out one row inside `width` columns.
1864
- *
1865
- * With no meta segment the line is simply truncated. With one, the meta is what must survive — a
1866
- * count that has been cut in half (`• 19 outpu…`) tells the reader nothing — so the left side gives
1867
- * up room first, and only a meta wider than the whole viewport is truncated itself.
1868
- */
1869
- function alignRow(line, width) {
1870
- const { left, right } = line;
1871
- if (right === void 0) return truncateToWidth(left, width, "…");
1872
- const rightWidth = visibleWidth(right);
1873
- if (rightWidth >= width) return truncateToWidth(right, width, "…");
1874
- const leftText = truncateToWidth(left, Math.max(0, width - rightWidth - 1), "…");
1875
- const pad = Math.max(1, width - rightWidth - visibleWidth(leftText));
1876
- return leftText + " ".repeat(pad) + right;
1877
- }
1878
- /** Truncate `s` to `n` chars, appending `…` when the input was longer. */
1879
- function truncateChars(s, n) {
1880
- return s.length > n ? `${s.slice(0, n - 1)}…` : s;
1881
- }
1882
- /** First non-blank line of a block of text, sanitized and capped. */
1883
- function firstLine(value) {
1884
- return truncateChars((sanitizeText(value).split("\n").find((candidate) => candidate.trim().length > 0) ?? "").trim(), MAX_ERROR_CHARS);
1885
- }
1886
- /** `n` rendered as `1.2s` / `850ms` (so users can tell short from long at a glance). */
1887
- function formatDuration(ms) {
1888
- if (ms < 1e3) return `${ms}ms`;
1889
- return `${(ms / 1e3).toFixed(2).replace(/\.?0+$/, "")}s`;
1890
- }
1891
- /** `3 phases` / `1 phase` — singular and plural are both reachable from a real run. */
1892
- function plural(count, noun) {
1893
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
1894
- }
1895
- /**
1896
- * Pull the first line of code that is actually doing something — not a leading comment, blank
1897
- * line, or import. Used as the call row's fallback when the model sent no description.
1898
- */
1899
- function firstMeaningfulCodeLine(code) {
1900
- for (const raw of code.split("\n")) {
1901
- const line = raw.trim();
1902
- if (line.length === 0) continue;
1903
- if (line.startsWith("//")) continue;
1904
- if (line.startsWith("/*")) continue;
1905
- if (line.startsWith("*")) continue;
1906
- return truncateChars(line.replace(/\s*\/\/.*$/, ""), MAX_CODE_LINE_CHARS);
1907
- }
1908
- }
1909
- /**
1910
- * The completion value as a single line a human can read: the value itself while it stays short
1911
- * and single-line, otherwise its shape (`{version, node}`, `Array(3)`).
1912
- *
1913
- * A multi-line string contributes its first non-blank line plus a `+N lines` tail, which is the
1914
- * whole difference between one readable row and the escaped wall this replaces.
1915
- */
1916
- function resultHint(value) {
1917
- if (value === void 0) return void 0;
1918
- if (typeof value === "string") {
1919
- const lines = sanitizeText(value).split("\n");
1920
- const first = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
1921
- const extra = lines.length > 1 ? ` (+${lines.length - 1} lines)` : "";
1922
- if (first.length === 0 && extra.length === 0) return `""`;
1923
- return truncateChars(first, MAX_RESULT_HINT_CHARS) + extra;
1924
- }
1925
- if (value === null || typeof value !== "object") return String(value);
1926
- const inline = renderModelValue(value);
1927
- if (!inline.includes("\n") && inline.length <= MAX_RESULT_HINT_CHARS) return inline;
1928
- if (Array.isArray(value)) return `Array(${value.length})`;
1929
- const keys = Object.keys(value);
1930
- if (keys.length === 0) return "{}";
1931
- return `{${keys.slice(0, 4).join(", ")}${keys.length > 4 ? `, +${keys.length - 4}` : ""}}`;
1932
- }
1933
- /**
1934
- * Right-aligned meta: the run's countable facts, in the order a reader asks for them — how much
1935
- * output, how long it took, how many phases (workflow), and anything that went wrong.
1936
- */
1937
- function metaText(details, theme) {
1938
- if (details === void 0) return void 0;
1939
- const segments = [];
1940
- const outputLines = details.logs.length + details.narrations.length;
1941
- if (outputLines > 0) segments.push(theme.fg("dim", plural(outputLines, "output line")));
1942
- if (details.imageCount > 0) segments.push(theme.fg("toolOutput", plural(details.imageCount, "image")));
1943
- if (details.surface === "workflow" && details.phases.length > 0) segments.push(theme.fg("dim", plural(details.phases.length, "phase")));
1944
- if (details.warnings.length > 0) segments.push(theme.fg("warning", plural(details.warnings.length, "warning")));
1945
- if (details.fullOutputPath !== void 0) segments.push(theme.fg("warning", "truncated"));
1946
- segments.push(theme.fg("dim", formatDuration(details.durationMs)));
1947
- return `${theme.fg("dim", "•")} ${segments.join(theme.fg("dim", " · "))}`;
1948
- }
1949
- /** Render one child block as `RowLine[]`, with `├─` / `│` / `└─` connectors decided by position. */
1950
- function renderChildBlock(child, isLast, theme) {
1951
- const firstPrefix = TREE_INDENT + (isLast ? TREE_LAST : TREE_FIRST);
1952
- const contPrefix = TREE_INDENT + (isLast ? TREE_CONT_LAST : TREE_CONT_FIRST);
1953
- const gutter = " ".repeat(LABEL_WIDTH);
1954
- const rows = [];
1955
- const first = child.lines[0] ?? "";
1956
- rows.push({ left: `${firstPrefix}${child.label.padEnd(LABEL_WIDTH)}${theme.fg("dim", first)}` });
1957
- for (let i = 1; i < child.lines.length; i++) rows.push({ left: `${contPrefix}${gutter}${theme.fg("dim", child.lines[i] ?? "")}` });
1958
- if (child.more !== void 0 && child.more > 0) rows.push({ left: `${contPrefix}${gutter}…+${child.more} more lines` });
1959
- return rows;
1960
- }
1961
- /** A JSON value is "expandable" when it is a non-empty object or array. */
1962
- function isExpandableContainer(value) {
1963
- if (value === null || typeof value !== "object") return false;
1964
- if (Array.isArray(value)) return value.length > 0;
1965
- return Object.keys(value).length > 0;
1966
- }
1967
- /** Object keys that read as identifiers are left unquoted; the rest get JSON quoting. */
1968
- const TREE_BARE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1969
- function treeFormatKey(key) {
1970
- return TREE_BARE_KEY.test(key) ? key : JSON.stringify(key);
1971
- }
1972
- /** A scalar JSON value as a single readable token; strings are JSON-quoted, newlines folded. */
1973
- function treeScalarPreview(value, maxChars) {
1974
- if (value === null) return "null";
1975
- if (typeof value === "boolean" || typeof value === "number") return String(value);
1976
- if (typeof value === "string") {
1977
- const sanitized = sanitizeText(value);
1978
- if (sanitized.length === 0) return "\"\"";
1979
- if (sanitized.includes("\n")) {
1980
- const head = truncateChars(sanitized.replace(/\n/g, " "), Math.max(2, maxChars - 5));
1981
- return JSON.stringify(head) + "…";
1982
- }
1983
- if (sanitized.length + 2 <= maxChars) return JSON.stringify(sanitized);
1984
- return JSON.stringify(truncateChars(sanitized, Math.max(2, maxChars - 5))) + "…";
1985
- }
1986
- return "?";
1987
- }
1988
- function treeIsScalarJson(value) {
1989
- return value === null || typeof value !== "object";
1990
- }
1991
- /** Try to render a container as a one-liner bracket form ({k: v} / [v, v]); undefined when it does not fit or children are deep. */
1992
- function treeInlinePreview(value, maxChars) {
1993
- if (maxChars < 8) return void 0;
1994
- if (Array.isArray(value)) {
1995
- if (value.length === 0) return "[]";
1996
- if (value.length > 5 || !value.every(treeIsScalarJson)) return void 0;
1997
- const inline = "[" + value.map((item) => treeScalarPreview(item, Math.max(2, Math.floor(maxChars / 4)))).join(", ") + "]";
1998
- return inline.length <= maxChars ? inline : void 0;
1999
- }
2000
- if (value === null || typeof value !== "object") return void 0;
2001
- const keys = Object.keys(value);
2002
- if (keys.length === 0) return "{}";
2003
- if (keys.length > 5 || !Object.values(value).every(treeIsScalarJson)) return void 0;
2004
- const inline = "{" + keys.map((key) => {
2005
- const child = value[key] ?? null;
2006
- return treeFormatKey(key) + ": " + treeScalarPreview(child, Math.max(2, Math.floor(maxChars / 4)));
2007
- }).join(", ") + "}";
2008
- return inline.length <= maxChars ? inline : void 0;
2009
- }
2010
- /**
2011
- * Append one pre-prefixed tree row to out, recursing for containers. Each level passes the
2012
- * continuation indent its children should use; the connector itself encodes "more siblings".
2013
- */
2014
- function treeAppendNode(value, label, parentPrefix, isLast, maxDepth, maxChildren, maxLineChars, depth, out) {
2015
- const ownPrefix = parentPrefix + (isLast ? "└─ " : "├─ ");
2016
- const continuation = parentPrefix + (isLast ? " " : "│ ");
2017
- if (depth >= maxDepth) {
2018
- out.push(treeTruncateIndent(ownPrefix + label + "…", maxLineChars));
2019
- return;
2020
- }
2021
- if (Array.isArray(value) && value.length === 0) {
2022
- out.push(treeTruncateIndent(ownPrefix + label + "[]", maxLineChars));
2023
- return;
2024
- }
2025
- if (!Array.isArray(value) && value !== null && typeof value === "object" && Object.keys(value).length === 0) {
2026
- out.push(treeTruncateIndent(ownPrefix + label + "{}", maxLineChars));
2027
- return;
2028
- }
2029
- if (value === null || typeof value !== "object") {
2030
- const budget = Math.max(0, maxLineChars - visibleWidth(ownPrefix) - label.length);
2031
- out.push(treeTruncateIndent(ownPrefix + label + treeScalarPreview(value, budget), maxLineChars));
2032
- return;
2033
- }
2034
- const inline = treeInlinePreview(value, Math.max(0, maxLineChars - visibleWidth(ownPrefix) - label.length));
2035
- if (inline !== void 0) {
2036
- out.push(treeTruncateIndent(ownPrefix + label + inline, maxLineChars));
2037
- return;
2038
- }
2039
- const listLike = Array.isArray(value);
2040
- const childCount = listLike ? value.length : Object.keys(value).length;
2041
- let header;
2042
- if (listLike) header = "Array(" + childCount + ")";
2043
- else header = childCount > maxChildren ? "{" + maxChildren + " of " + childCount + "}" : "{" + childCount + " keys}";
2044
- out.push(treeTruncateIndent(ownPrefix + label + header, maxLineChars));
2045
- const limit = Math.min(maxChildren, childCount);
2046
- if (listLike) for (let index = 0; index < limit; index += 1) {
2047
- const isChildLast = index === limit - 1 && childCount <= limit;
2048
- treeAppendNode(value[index] ?? null, "[" + index + "] ", continuation, isChildLast, maxDepth, maxChildren, maxLineChars, depth + 1, out);
2049
- }
2050
- else {
2051
- const keys = Object.keys(value);
2052
- for (let index = 0; index < limit; index += 1) {
2053
- const isChildLast = index === limit - 1 && childCount <= limit;
2054
- const key = keys[index];
2055
- treeAppendNode(value[key] ?? null, treeFormatKey(key) + ": ", continuation, isChildLast, maxDepth, maxChildren, maxLineChars, depth + 1, out);
2056
- }
2057
- }
2058
- if (childCount > limit) {
2059
- const tail = listLike ? "items" : "keys";
2060
- out.push(treeTruncateIndent(continuation + "└─ …+" + (childCount - limit) + " more " + tail, maxLineChars));
2061
- }
2062
- }
2063
- /** Truncate a rendered row, preserving the leading tree-prefix indent (│/├/└ etc.). */
2064
- function treeTruncateIndent(line, maxLineChars) {
2065
- if (visibleWidth(line) <= maxLineChars) return line;
2066
- const match = line.match(/^([\s│├└─]*)/);
2067
- const indent = match && match[1] ? match[1] : "";
2068
- const indentWidth = visibleWidth(indent);
2069
- const budget = Math.max(0, maxLineChars - indentWidth - 1);
2070
- let body = "";
2071
- let used = 0;
2072
- for (const ch of line.slice(indent.length)) {
2073
- const w = visibleWidth(ch);
2074
- if (used + w > budget) break;
2075
- body += ch;
2076
- used += w;
2077
- }
2078
- return indent + body + "…";
2079
- }
2080
- /**
2081
- * Render a JSON value as an array of indented tree rows.
2082
- *
2083
- * Each row is a complete prefixed line ready to live under the call row's " " indent
2084
- * (TREE_ROOT_CONT): root children start at "├─ " / "└─ ", deeper levels add their own
2085
- * connectors on top of the continuation bar. Caller supplies the row's TREE_ROOT_CONT — a
2086
- * row that starts with "├─ " lands at column 3, exactly where other call-row children sit.
2087
- *
2088
- * Empty / scalar values return a one-row preview; non-empty containers recurse.
2089
- */
2090
- function renderValueTree(value, options = {}) {
2091
- const maxDepth = options.maxDepth ?? 4;
2092
- const maxChildren = options.maxChildren ?? 6;
2093
- const maxLineChars = options.maxLineChars ?? 120;
2094
- const moreAfter = options.moreAfter === true;
2095
- if (!isExpandableContainer(value)) {
2096
- if (value !== null && typeof value === "object") return [Array.isArray(value) ? "[]" : "{}"];
2097
- return [treeScalarPreview(value, maxLineChars)];
2098
- }
2099
- const out = [];
2100
- if (Array.isArray(value)) {
2101
- const total = value.length;
2102
- const limit = Math.min(maxChildren, total);
2103
- for (let index = 0; index < limit; index += 1) {
2104
- const isLast = !moreAfter && index === limit - 1 && total <= limit;
2105
- treeAppendNode(value[index] ?? null, "[" + index + "] ", "", isLast, maxDepth, maxChildren, maxLineChars, 0, out);
2106
- }
2107
- if (total > limit) out.push((moreAfter ? "├─ " : "└─ ") + "…+" + (total - limit) + " more items");
2108
- return out;
2109
- }
2110
- const keys = Object.keys(value);
2111
- const total = keys.length;
2112
- const limit = Math.min(maxChildren, total);
2113
- for (let index = 0; index < limit; index += 1) {
2114
- const isLast = !moreAfter && index === limit - 1 && total <= limit;
2115
- const key = keys[index];
2116
- treeAppendNode(value[key] ?? null, treeFormatKey(key) + ": ", "", isLast, maxDepth, maxChildren, maxLineChars, 0, out);
2117
- }
2118
- if (total > limit) out.push((moreAfter ? "├─ " : "└─ ") + "…+" + (total - limit) + " more keys");
2119
- return out;
2120
- }
2121
- /**
2122
- * One-line tool-call header: the tree root, then the tool label, then the model's own description.
2123
- *
2124
- * The description is the only detail that gets to sit next to the label — the previous renderer
2125
- * appended the first code line as well, which is what made the call row read as a paragraph rather
2126
- * than a heading. The code is one `ctrl+e` away in the expanded view.
2127
- */
2128
- function renderPtcToolCall(args, theme, surface = "run_code") {
2129
- const description = (typeof args.description === "string" && args.description.trim().length > 0 ? args.description.trim() : args.meta?.name) ?? "";
2130
- const fallback = firstMeaningfulCodeLine(args.code ?? args.script ?? "");
2131
- const detail = description.length > 0 ? description : fallback ?? "(no description)";
2132
- const left = `${theme.fg("toolTitle", theme.bold(TOOL_TITLE[surface]))} ${theme.fg("accent", detail)}`;
2133
- return new PtcRow([{ left: `${TREE_ROOT}${left}` }]);
2134
- }
2135
- /** Text body of a failed run — pi surfaces the thrown error in the first text block. */
2136
- function errorText(result) {
2137
- const text = result.content?.find((block) => block.type === "text" && (block.text ?? "").length > 0);
2138
- return firstLine(text?.text ?? "") || "failed";
2139
- }
2140
- /**
2141
- * The result area under the call row.
2142
- *
2143
- * One of four shapes, all hanging at column 3 so a column of PTC rows stays scannable:
2144
- * - error: one line with the failure reason in the error colour.
2145
- * - no completion value: one line, "done".
2146
- * - scalar / empty container: one line, `→ <hint>` (or "done" for null).
2147
- * - non-empty container: a tree of rows (recursive `├─/└─/│` connectors from
2148
- * `renderValueTree`), with the right-aligned meta pinned to
2149
- * the tree's first row. ADR-0013 §5.
2150
- *
2151
- * Each row returns its left-side text WITHOUT the `TREE_ROOT_CONT` prefix being in scope;
2152
- * we prepend it here so the same output works for both surfaces.
2153
- */
2154
- function resultArea(result, isError, theme, moreAfter = false) {
2155
- const right = metaText(result.details, theme);
2156
- const attachRight = (row) => right === void 0 ? row : {
2157
- ...row,
2158
- right
2159
- };
2160
- if (isError) return [attachRight({ left: `${TREE_ROOT_CONT}${theme.fg("error", `failed: ${errorText(result)}`)}` })];
2161
- const value = result.details?.result;
2162
- if (value === void 0) return [attachRight({ left: `${TREE_ROOT_CONT}${theme.fg("muted", "done")}` })];
2163
- if (isExpandableContainer(value)) return renderValueTree(value, {
2164
- maxDepth: 4,
2165
- maxChildren: 6,
2166
- maxLineChars: 120,
2167
- moreAfter
2168
- }).map((line, index) => {
2169
- const row = { left: `${TREE_ROOT_CONT}${line}` };
2170
- return index === 0 && right !== void 0 ? {
2171
- ...row,
2172
- right
2173
- } : row;
2174
- });
2175
- const hint = resultHint(value);
2176
- if (hint === void 0) return [attachRight({ left: `${TREE_ROOT_CONT}${theme.fg("muted", "done")}` })];
2177
- return [attachRight({ left: `${TREE_ROOT_CONT}${theme.fg("dim", `→ ${hint}`)}` })];
2178
- }
2179
- /**
2180
- * Collapsed result: the result area only.
2181
- *
2182
- * A scalar row fits on one line; a container value expands into a tree (1+ lines). Right-aligned
2183
- * meta (output lines / phases / warnings / duration) is pinned to the first row of the area.
2184
- */
2185
- function renderPtcToolResultCollapsed(result, isError, theme) {
2186
- return new PtcRow(resultArea(result, isError, theme));
2187
- }
2188
- /**
2189
- * Expanded view: the result area, then the run's children as labelled blocks.
2190
- *
2191
- * Each child block opens with a fixed-width gutter (`code`, `phases`, `log`, `out`, `warn`,
2192
- * `image`); the `value` block is gone — for a scalar value the result area already shows the
2193
- * hint, and for a container value the area shows the full tree (ADR-0013 §5). Children that
2194
- * overflow cap themselves and report `+N more lines`; the view stays bounded.
2195
- */
2196
- function renderPtcToolResultExpanded(result, args, isError, theme) {
2197
- if (isError) {
2198
- const lines = resultArea(result, true, theme);
2199
- const text = sanitizeText(result.content?.find((block) => block.type === "text")?.text ?? errorText(result));
2200
- for (const line of text.split("\n").slice(0, MAX_LOG_LINES_EXPANDED)) lines.push({ left: `${TREE_ROOT_CONT}${theme.fg("error", line)}` });
2201
- return new PtcRow(lines);
2202
- }
2203
- const details = result.details;
2204
- const children = [];
2205
- const code = args.code ?? args.script ?? "";
2206
- if (code.length > 0) {
2207
- const codeLines = code.split("\n").map((line) => truncateChars(line, MAX_CODE_LINE_CHARS));
2208
- const shown = codeLines.slice(0, MAX_CODE_LINES_EXPANDED);
2209
- children.push({
2210
- label: "code",
2211
- lines: shown,
2212
- ...codeLines.length > shown.length ? { more: codeLines.length - shown.length } : {}
2213
- });
2214
- }
2215
- if (details !== void 0 && details.surface === "workflow" && details.phases.length > 0) {
2216
- const shown = details.phases.slice(0, MAX_PHASES_EXPANDED);
2217
- const tail = details.phases.length > shown.length ? " …" : "";
2218
- children.push({
2219
- label: "phases",
2220
- lines: [`${shown.join(" → ")}${tail}`]
2221
- });
2222
- }
2223
- if (details !== void 0 && details.narrations.length > 0) {
2224
- const shown = details.narrations.slice(0, MAX_LOG_LINES_EXPANDED);
2225
- children.push({
2226
- label: "log",
2227
- lines: shown.map((message) => sanitizeText(message)),
2228
- ...details.narrations.length > shown.length ? { more: details.narrations.length - shown.length } : {}
2229
- });
2230
- }
2231
- if (details !== void 0 && details.logs.length > 0) {
2232
- const shown = details.logs.slice(0, MAX_LOG_LINES_EXPANDED);
2233
- children.push({
2234
- label: "out",
2235
- lines: shown.map((line) => sanitizeText(line)),
2236
- ...details.logs.length > shown.length ? { more: details.logs.length - shown.length } : {}
2237
- });
2238
- }
2239
- if (details !== void 0 && details.warnings.length > 0) {
2240
- const shown = details.warnings.slice(0, MAX_WARNINGS_EXPANDED);
2241
- children.push({
2242
- label: "warn",
2243
- lines: shown.map((warning) => sanitizeText(warning)),
2244
- ...details.warnings.length > shown.length ? { more: details.warnings.length - shown.length } : {}
2245
- });
2246
- }
2247
- if (details !== void 0 && details.imageCount > 0) {
2248
- const desc = details.imageCount === 1 ? "1 image attached" : `${details.imageCount} images attached`;
2249
- children.push({
2250
- label: "image",
2251
- lines: [desc]
2252
- });
2253
- }
2254
- const lines = resultArea(result, false, theme, children.length > 0);
2255
- for (let index = 0; index < children.length; index += 1) {
2256
- const isLast = index === children.length - 1;
2257
- lines.push(...renderChildBlock(children[index] ?? {
2258
- label: "",
2259
- lines: []
2260
- }, isLast, theme));
2261
- }
2262
- return new PtcRow(lines);
2263
- }
2264
- //#endregion
2265
- //#region src/tools/run-code.ts
2266
- /**
2267
- * `ptc_run_code` — the unstructured PTC surface.
2268
- *
2269
- * The model writes the body of an async function; inside it, pi's built-in tools are reachable as
2270
- * `tools.<name>(args)` (all seven: read, bash, edit, write, grep, find, ls — ADR-0005's set), and
2271
- * the program's return value plus `console.log` output are what come back. There are no helpers
2272
- * on this surface: `log` / `phase` / `parallel` / `pipeline` belong to `ptc_workflow` (G1 #13 →
2273
- * decision B; a call to a helper that does not exist is a plain `ReferenceError` through the
2274
- * normal code-run error path).
2275
- *
2276
- * Scope notes, so this stays a faithful subset of DSH's `run_code`:
2277
- * - DSH's `sandbox_permissions` / `justification` are deliberately absent — they exist to request
2278
- * a wider sandbox from an approval pipeline this package does not have (ADR-0007 ships no OS
2279
- * sandbox, and the map decision on #8 dropped the approval path).
2280
- * - `timeoutMs: 0` does not disable the deadline; it falls back to `DEFAULT_CONFIG.timeoutMs`,
2281
- * exactly as DSH's resolver does (R1 §3). The description names the default and the ceiling.
2282
- */
2283
- const DESCRIPTION$1 = [
2284
- "Run a TypeScript program that composes pi's tools in one shot. Required arguments: `code` —",
2285
- "the body of an async function (top-level `return` and `await` work; type annotations are",
2286
- "advisory, the code runs type-stripped) — and `description`, a 5-10 word summary of what the",
2287
- "program does.",
2288
- "",
2289
- "Inside the program, call this session's enabled built-in tools as `tools.<name>(args)` — e.g.",
2290
- "`await tools.read({ path: \"src/index.ts\" })` or `await tools.bash({ command: \"npm test\" })`.",
2291
- "The bound names mirror the session's active tools (a default session has `read`, `bash`, `edit`,",
2292
- "`write`); calling a name that is not bound rejects with an error the program can catch, and",
2293
- "independent calls may overlap under `Promise.all`.",
2294
- "",
2295
- "Only the program's return value and its `console.log` output come back. This surface has no",
2296
- "helpers: `log` / `phase` / `parallel` / `pipeline` exist only in `ptc_workflow`.",
2297
- "",
2298
- "Image-bearing tool results inside the program (a `tools.read` on a PNG, say) are attached to you",
2299
- "after the run, so never return image data as the completion value — that only spends your context",
2300
- "on base64."
2301
- ].join("\n");
2302
- const PARAMETERS$1 = Type.Object({
2303
- code: Type.String({ description: "The program: the body of an async TypeScript function." }),
2304
- description: Type.String({ description: "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI)." }),
2305
- timeoutMs: Type.Optional(Type.Number({ description: `Elapsed deadline in milliseconds for the whole run, including tool calls. Omit it (or pass 0) for the default of ${DEFAULT_CONFIG.timeoutMs / 1e3} s; anything above ${DEFAULT_CONFIG.maxTimeoutMs / 1e3} s is capped.` }))
2306
- });
2307
- /**
2308
- * Build the `ptc_run_code` tool definition.
2309
- *
2310
- * Called once by the extension factory. Each execute builds its own binding table against the
2311
- * run's own cwd and reads `getPool()` for the surface's (possibly absent) pool — see
2312
- * `PtcRunCodeToolOptions` for what pooling does and does not change about isolation.
2313
- */
2314
- function createPtcRunCodeTool(options = {}) {
2315
- return defineTool({
2316
- name: "ptc_run_code",
2317
- label: "PTC Run Code",
2318
- description: DESCRIPTION$1,
2319
- promptSnippet: PTC_RUN_CODE_SNIPPET,
2320
- promptGuidelines: [...PTC_TOOL_GUIDELINES],
2321
- parameters: PARAMETERS$1,
2322
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
2323
- const cwd = resolveToolCwd(ctx);
2324
- const names = resolveBindingNames(options.getBindingSourceNames?.());
2325
- const startedAt = Date.now();
2326
- const pool = options.getPool?.();
2327
- const outcome = await runPtcProgram({
2328
- code: params.code,
2329
- surface: "run_code",
2330
- cwd,
2331
- bindings: createBuiltinBindings({
2332
- cwd,
2333
- names,
2334
- includeDispatch: true
2335
- }),
2336
- ...options.depth === void 0 ? {} : { depth: options.depth },
2337
- ...params.timeoutMs === void 0 ? {} : { timeoutMs: params.timeoutMs },
2338
- ...signal === void 0 ? {} : { signal },
2339
- ...options.config === void 0 ? {} : { config: options.config },
2340
- ...pool === void 0 ? {} : { pool }
2341
- });
2342
- if (outcome.error !== void 0) throw codeRunFailedError(outcome);
2343
- return renderToolResult({
2344
- outcome,
2345
- surface: "run_code",
2346
- durationMs: Date.now() - startedAt
2347
- });
2348
- },
2349
- renderCall(args, theme) {
2350
- return renderPtcToolCall(args, theme, "run_code");
2351
- },
2352
- renderResult(result, options, theme, context) {
2353
- if (options.expanded) return renderPtcToolResultExpanded(result, context.args, context.isError, theme);
2354
- return renderPtcToolResultCollapsed(result, context.isError, theme);
2355
- }
2356
- });
2357
- }
2358
- //#endregion
2359
- //#region src/tools/workflow.ts
2360
- /**
2361
- * `ptc_workflow` — the structured PTC surface.
2362
- *
2363
- * Same worker program, same bindings, same Node surface as `ptc_run_code`; what it adds is a plan
2364
- * (`meta` with an ordered phase list), plain-JSON input (`args`, bound as the program's `args`
2365
- * global) and the four workflow helpers: `log` / `phase` / `parallel` / `pipeline` (G1 #13 →
2366
- * decision B — there is no `agent()`, not even a stub).
2367
- *
2368
- * Three decisions recorded here rather than re-litigated per call:
2369
- * - **Language**: the same type-stripping pipeline as `ptc_run_code`. The worker compiles both
2370
- * surfaces through one `compileProgram`, and the pipeline is type-*strip* only — it adds no
2371
- * runtime semantics that a workflow could not tolerate. There is no JS-only mode because there
2372
- * is no concrete reason for one (the old pi-ptc "JS-only" idea is explicitly not inherited).
2373
- * - **Phase strictness**: warning, not error. A `phase(title)` outside `meta.phases` is a plan
2374
- * drift — usually a typo or a forgotten declaration — but the run has already happened by the
2375
- * time the titles are known, and failing then would discard a completed result over a cosmetic
2376
- * mismatch. Each distinct unlisted title is reported once, as a `Warning:` line in `content`
2377
- * and in `details.warnings`. When `meta.phases` is absent there is no declared plan to drift
2378
- * from, so nothing is reported.
2379
- * - **`args`**: validated as plain JSON before dispatch (see `validateWorkflowArgs`), because a
2380
- * function/symbol/`undefined`/cycle would otherwise be silently dropped or mangled by the
2381
- * structured clone on its way to the worker.
2382
- */
2383
- const DESCRIPTION = [
2384
- "Run a structured TypeScript workflow: a named plan that reports phases and narration as it",
2385
- "goes. `script` is the body of an async function, `meta` names it (`{ name, description, phases? }`,",
2386
- "where `phases` is the ordered plan), and optional `args` is plain-JSON input bound to the",
2387
- "program's `args` global.",
2388
- "",
2389
- "Helpers inside the script: `log(message)` narrates progress; `phase(title)` frames the workflow",
2390
- "(titles outside `meta.phases` are reported as warnings); `parallel(thunks)` runs thunks",
2391
- "concurrently, a failed item becoming `null`; `pipeline(items, ...stages)` chains stages per item",
2392
- "with the same per-item `null` on failure. There is no `agent()` helper.",
2393
- "",
2394
- "Tools are reachable as `tools.<name>(args)` exactly as in `ptc_run_code`, and the bound names",
2395
- "mirror the session's enabled tools. What comes back is the script's return value, its",
2396
- "`log`/`phase` narration and its `console.log` output.",
2397
- "",
2398
- "Image-bearing tool results inside the script are attached to you after the run — never return",
2399
- "image data as the script's value."
2400
- ].join("\n");
2401
- const PARAMETERS = Type.Object({
2402
- meta: Type.Object({
2403
- name: Type.String({ description: "Short workflow name, shown in the UI." }),
2404
- description: Type.String({ description: "What this workflow does, in active voice." }),
2405
- phases: Type.Optional(Type.Array(Type.Object({ name: Type.String({ description: "Phase title, matched against phase(title) calls." }) }), { description: "Ordered plan of the workflow's phases. phase() titles outside this list are reported as warnings." }))
2406
- }, { description: "Workflow metadata: what this run is and which phases it declares." }),
2407
- script: Type.String({ description: "The program: the body of an async TypeScript function." }),
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." }))
2409
- });
2410
- /**
2411
- * Reject anything that is not plain JSON, with a path-qualified reason.
2412
- *
2413
- * The rule is the worker's own result rule (`toJsonValue`) applied one direction earlier, so both
2414
- * ends agree on what "plain JSON" means: objects with `Object.prototype` (or null), arrays,
2415
- * strings, finite numbers, booleans and null. Class instances, functions, symbols, bigints,
2416
- * non-finite numbers, circular references and `undefined` values are all rejected — `undefined`
2417
- * because the ticket calls it out and because a structured clone would drop it silently, leaving
2418
- * the script reading a key that is not there.
2419
- */
2420
- function validateWorkflowArgs(args) {
2421
- if (args === void 0) return;
2422
- if (typeof args !== "object" || args === null || Array.isArray(args)) throw new TypeError(`ptc_workflow args must be a plain JSON object, received ${describeValue(args)}`);
2423
- for (const key of Object.keys(args)) {
2424
- const problem = findJsonProblem(args[key], `args.${key}`, /* @__PURE__ */ new Set());
2425
- if (problem !== void 0) throw new TypeError(`ptc_workflow args must be plain JSON: ${problem}`);
2426
- }
2427
- }
2428
- /** Path-qualified reason a value cannot cross the wire as plain JSON, or `undefined` when it can. */
2429
- function findJsonProblem(value, path, seen) {
2430
- if (value === null || typeof value === "string" || typeof value === "boolean") return void 0;
2431
- if (typeof value === "undefined") return `${path} is undefined`;
2432
- if (typeof value === "number") return Number.isFinite(value) ? void 0 : `${path} is ${String(value)}, which is not representable as JSON`;
2433
- if (typeof value !== "object") return `${path} is ${describeValue(value)}`;
2434
- const container = value;
2435
- if (seen.has(container)) return `${path} is a circular reference`;
2436
- seen.add(container);
2437
- try {
2438
- if (Array.isArray(container)) {
2439
- for (let index = 0; index < container.length; index += 1) {
2440
- const problem = findJsonProblem(container[index], `${path}[${index}]`, seen);
2441
- if (problem !== void 0) return problem;
2442
- }
2443
- return;
2444
- }
2445
- const prototype = Object.getPrototypeOf(container);
2446
- if (prototype !== Object.prototype && prototype !== null) {
2447
- const name = container.constructor?.name;
2448
- return `${path} is a ${typeof name === "string" && name.length > 0 ? name : "non-plain object"}, not plain JSON`;
2449
- }
2450
- for (const key of Object.keys(container)) {
2451
- const problem = findJsonProblem(container[key], `${path}.${key}`, seen);
2452
- if (problem !== void 0) return problem;
2453
- }
2454
- return;
2455
- } finally {
2456
- seen.delete(container);
2457
- }
2458
- }
2459
- /**
2460
- * Collect the plan-drift warnings for one workflow run: distinct `phase()` titles the declared
2461
- * `meta.phases` list does not mention, in first-seen order.
2462
- */
2463
- function unlistedPhaseWarnings(outcome, declared) {
2464
- if (declared === void 0 || declared.length === 0) return [];
2465
- const known = new Set(declared);
2466
- const reported = /* @__PURE__ */ new Set();
2467
- const warnings = [];
2468
- for (const title of outcome.phases) {
2469
- if (known.has(title) || reported.has(title)) continue;
2470
- reported.add(title);
2471
- warnings.push(`phase "${title}" is not listed in meta.phases (declared: ${[...known].join(", ")})`);
2472
- }
2473
- return warnings;
2474
- }
2475
- /**
2476
- * Build the `ptc_workflow` tool definition.
2477
- *
2478
- * `args` is validated before `runPtcProgram` is called, so a malformed payload never acquires or
2479
- * spawns a worker. Everything after dispatch is identical to `ptc_run_code`, plus the phase
2480
- * roll-up.
2481
- */
2482
- function createPtcWorkflowTool(options = {}) {
2483
- return defineTool({
2484
- name: "ptc_workflow",
2485
- label: "PTC Workflow",
2486
- description: DESCRIPTION,
2487
- promptSnippet: PTC_WORKFLOW_SNIPPET,
2488
- promptGuidelines: [...PTC_TOOL_GUIDELINES],
2489
- parameters: PARAMETERS,
2490
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
2491
- validateWorkflowArgs(params.args);
2492
- const cwd = resolveToolCwd(ctx);
2493
- const names = resolveBindingNames(options.getBindingSourceNames?.());
2494
- const startedAt = Date.now();
2495
- const pool = options.getPool?.();
2496
- const outcome = await runPtcProgram({
2497
- code: params.script,
2498
- surface: "workflow",
2499
- cwd,
2500
- bindings: createBuiltinBindings({
2501
- cwd,
2502
- names,
2503
- includeDispatch: true
2504
- }),
2505
- ...params.args === void 0 ? {} : { args: params.args },
2506
- ...options.depth === void 0 ? {} : { depth: options.depth },
2507
- ...signal === void 0 ? {} : { signal },
2508
- ...options.config === void 0 ? {} : { config: options.config },
2509
- ...pool === void 0 ? {} : { pool }
2510
- });
2511
- if (outcome.error !== void 0) throw codeRunFailedError(outcome);
2512
- const declared = params.meta.phases?.map((phase) => phase.name);
2513
- return renderToolResult({
2514
- outcome,
2515
- surface: "workflow",
2516
- warnings: unlistedPhaseWarnings(outcome, declared),
2517
- durationMs: Date.now() - startedAt
2518
- });
2519
- },
2520
- renderCall(args, theme) {
2521
- return renderPtcToolCall(args, theme, "workflow");
2522
- },
2523
- renderResult(result, options, theme, context) {
2524
- if (options.expanded) return renderPtcToolResultExpanded(result, context.args, context.isError, theme);
2525
- return renderPtcToolResultCollapsed(result, context.isError, theme);
2526
- }
2527
- });
2528
- }
2529
- //#endregion
2530
- //#region src/runtime/turn-pools.ts
2531
- /**
2532
- * Per-turn pool holder: one `WorkerPool` per PTC surface, created lazily on first use
2533
- * and retired together at turn end (ADR-0017 §1–§2).
2534
- *
2535
- * The pi extension owns one `TurnPools` per agent turn and hands each tool a getter for
2536
- * its own surface's pool. Lazy creation is what makes the cold turn cheap: a turn that
2537
- * never runs a PTC program never spawns a worker, and a turn that runs one `ptc_run_code`
2538
- * pays exactly one cold spawn (which the pool then keeps warm for the rest of the turn).
2539
- *
2540
- * `run_code` and `workflow` do not share a pool (ADR-0017 §2): their worker surfaces differ
2541
- * (`installWorkflowHelpers` is conditional in `worker-main.ts`), and one pool per surface
2542
- * keeps that story simple. The workers themselves carry no surface state — each run's
2543
- * `init` frame names its surface — so the split is about ownership, not correctness.
2544
- */
2545
- var TurnPools = class {
2546
- #config;
2547
- #pools = /* @__PURE__ */ new Map();
2548
- constructor(options = {}) {
2549
- this.#config = resolveConfig(options.config);
2550
- }
2551
- /**
2552
- * The pool for one surface, created on first request. Warm workers from an earlier
2553
- * run in this turn are served by the same pool (that is the point); a fresh
2554
- * `TurnPools` is how a turn gets a fresh set.
2555
- */
2556
- get(surface) {
2557
- const existing = this.#pools.get(surface);
2558
- if (existing !== void 0) return existing;
2559
- const env = createWorkerEnv();
2560
- const pool = new WorkerPool({
2561
- buildWorkerUrl,
2562
- size: this.#config.poolSize,
2563
- acquireTimeoutMs: this.#config.poolAcquireTimeoutMs,
2564
- drainGraceMs: this.#config.drainGraceMs,
2565
- workerOptions: workerSpawnOptions({
2566
- surface,
2567
- env,
2568
- limits: this.#config
2569
- })
2570
- });
2571
- this.#pools.set(surface, pool);
2572
- return pool;
2573
- }
2574
- /**
2575
- * Terminate every pool this holder created. Idempotent, and safe to call from a turn
2576
- * boundary hook: a second call finds nothing to drain.
2577
- */
2578
- async drain() {
2579
- const pools = [...this.#pools.values()];
2580
- this.#pools.clear();
2581
- await Promise.all(pools.map((pool) => pool.drain()));
2582
- }
2583
- };
2584
- //#endregion
2585
- //#region src/mode/ptc-mode.ts
2586
- /**
2587
- * PTC default mode — the session-level tool-restriction state machine.
2588
- *
2589
- * ## What "mode" means here
2590
- *
2591
- * While enabled, the session's visible tool loadout is narrowed so the model reaches
2592
- * `read` / `bash` / `edit` / `write` / `grep` / `find` / `ls` only from *inside* a program
2593
- * (`tools.<name>(args)`). That is DSH's PTC-preset shape, and it is what makes the mode visible
2594
- * in the TUI: the transcript stops showing file-operation rows and shows PTC rows instead.
2595
- *
2596
- * The mode is decided per session (`session_start`), announced once, and always reversible.
2597
- *
2598
- * ## Why bindings need a base snapshot (the trap this module exists to avoid)
2599
- *
2600
- * `resolveBindingNames()` computes bindings as `BUILTIN_BINDING_NAMES ∩ pi.getActiveTools()`
2601
- * (T7, ADR-0005 addendum). But `pi.getActiveTools()` reads the *live* loadout — the very set this
2602
- * mode narrows. Hiding the built-ins therefore empties the binding table, and every
2603
- * `tools.read(...)` fails with "not bound": the extension would neuter itself.
2604
- *
2605
- * The fix is a **base snapshot**. `base` is captured *before* the mode hides anything, and
2606
- * bindings derive from `base` rather than the live loadout while the mode is on. T7's invariant
2607
- * survives, because `base` is itself read from `pi.getActiveTools()`: a session launched with
2608
- * `--tools read` has `base = ["read"]`, so bindings stay `["read"]`. The mode never widens access
2609
- * beyond what the session was launched with — it only makes its **own** narrowing transparent to
2610
- * the binding table.
2611
- *
2612
- * ## Fail-safe on external loadout changes
2613
- *
2614
- * Another extension can call `setActiveTools()` (pi's own `preset.ts` and `tools.ts` examples do).
2615
- * `ourLoadout` records the exact array this mode wrote, so a mismatch means someone else owns the
2616
- * loadout now. The mode then exits and restores `base` instead of fighting over it.
2617
- *
2618
- * ## Where the mode never runs
2619
- *
2620
- * Print / JSON / RPC sessions (`ctx.mode !== "tui"`) are left alone: forcing the shape on CI
2621
- * scripts buys nothing and breaks them. A session that was *launched* with an explicit tool
2622
- * restriction is left alone too — see `decideModeEntry`.
2623
- */
2624
- /** The two surfaces this package exposes. `/ptc on` needs at least one of them active. */
2625
- const PTC_MODE_TOOL_NAMES = ["ptc_run_code", "ptc_workflow"];
2626
- /**
2627
- * Tools that must all be present for the **automatic** entry path to proceed.
2628
- *
2629
- * These four are pi's default session surface (`agent-session.js`: "["read", "bash", "edit",
2630
- * "write"]"), so "all four present" means "this session was not narrowed". Comparing against
2631
- * `BUILTIN_BINDING_NAMES` instead would be wrong: that list is the set of names that *can* be
2632
- * bound, not what a default session has (`grep` / `find` / `ls` are bindable but off by default),
2633
- * so requiring all seven would misread every ordinary session as restricted and never turn on.
2634
- */
2635
- const MODE_REQUIRED_TOOL_NAMES = [
2636
- "read",
2637
- "bash",
2638
- "edit",
2639
- "write"
2640
- ];
2641
- /** `customType` for the persisted mode entry (`pi.appendEntry`). */
2642
- const PTC_MODE_ENTRY_TYPE = "ptc-mode";
2643
- /** Footer status key (`ctx.ui.setStatus`). */
2644
- const PTC_MODE_STATUS_KEY = "ptc-mode";
2645
- /** Config file name, resolved against pi's agent dir (`~/.pi/agent/`). */
2646
- const PTC_MODE_CONFIG_FILE = "ptc.json";
2647
- /**
2648
- * Which tools the mode hides while it is on.
2649
- *
2650
- * - `"builtins-only"` (default) — hide the seven built-in tools; tools contributed by *other*
2651
- * extensions (`web_search`, `todo`, `subagent`, …) stay visible and directly callable. The
2652
- * model is forced to program for file and shell work, which is the bulk of a coding session,
2653
- * without losing capabilities this package cannot re-expose: extension tools have no bindings
2654
- * (`pi.getAllTools()` returns metadata only — no `execute`), so hiding one makes it
2655
- * unreachable for the whole session.
2656
- * - `"all-but-ptc"` — hide everything except the two PTC surfaces. Closest to a strict reading of
2657
- * DSH's preset, at the cost of making other extensions' tools unreachable until `/ptc off`.
2658
- */
2659
- const DEFAULT_HIDE_STRATEGY = "builtins-only";
2660
- /** Fresh, disabled state. */
2661
- function initialModeState() {
2662
- return {
2663
- enabled: false,
2664
- base: void 0,
2665
- ourLoadout: void 0
2666
- };
2667
- }
2668
- /**
2669
- * Read `defaultMode` from the agent-dir config file.
2670
- *
2671
- * Absent file → on (this package is default-on by design; see CONTEXT.md). A present file with a
2672
- * non-boolean `defaultMode`, or unparseable JSON, is reported as `invalid` **and still defaults to
2673
- * on** — the caller should surface the problem rather than silently changing behavior.
2674
- */
2675
- function readDefaultModeConfig(agentDir) {
2676
- const path = join(agentDir, PTC_MODE_CONFIG_FILE);
2677
- let raw;
2678
- try {
2679
- raw = readFileSync(path, "utf8");
2680
- } catch {
2681
- return {
2682
- defaultMode: true,
2683
- source: "default"
2684
- };
2685
- }
2686
- let parsed;
2687
- try {
2688
- parsed = JSON.parse(raw);
2689
- } catch (error) {
2690
- return {
2691
- defaultMode: true,
2692
- source: "invalid",
2693
- error: `${path} is not valid JSON (${error instanceof Error ? error.message : String(error)})`
2694
- };
2695
- }
2696
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {
2697
- defaultMode: true,
2698
- source: "invalid",
2699
- error: `${path} must contain a JSON object`
2700
- };
2701
- const value = parsed.defaultMode;
2702
- if (value === void 0) return {
2703
- defaultMode: true,
2704
- source: "default"
2705
- };
2706
- if (typeof value !== "boolean") return {
2707
- defaultMode: true,
2708
- source: "invalid",
2709
- error: `${path}: "defaultMode" must be a boolean, received ${typeof value}`
2710
- };
2711
- return {
2712
- defaultMode: value,
2713
- source: "file"
2714
- };
2715
- }
2716
- /** Normalize to a de-duplicated name list, preserving order. */
2717
- function uniqueNames(names) {
2718
- return [...new Set(names)];
2719
- }
2720
- /** Order- and duplicate-insensitive set equality, for loadout comparison. */
2721
- function sameToolSet(a, b) {
2722
- if (a.length !== b.length) return false;
2723
- const left = [...a].sort();
2724
- const right = [...b].sort();
2725
- return left.every((name, index) => name === right[index]);
2726
- }
2727
- /**
2728
- * The loadout applied while the mode is on, given the session's current one.
2729
- *
2730
- * Always keeps the PTC surfaces; what else survives depends on the hide strategy (see
2731
- * `DEFAULT_HIDE_STRATEGY`). Order follows `active`, so the visible list reads the way the session
2732
- * was configured.
2733
- */
2734
- function modeLoadout(active, hide) {
2735
- const isPtc = (name) => PTC_MODE_TOOL_NAMES.includes(name);
2736
- if (hide === "all-but-ptc") return uniqueNames(active.filter(isPtc));
2737
- const builtins = new Set(BUILTIN_BINDING_NAMES);
2738
- return uniqueNames(active.filter((name) => isPtc(name) || !builtins.has(name)));
2739
- }
2740
- /**
2741
- * Decide whether the mode turns on for this session.
2742
- *
2743
- * The policy, in order:
2744
- * 1. **TUI only** — print / JSON / RPC sessions are never touched.
2745
- * 2. **Config** — `{"defaultMode": false}` opts out; `/ptc on` overrides it.
2746
- * 3. **PTC tools must exist** — if this session has them disabled, there is nothing to run.
2747
- * 4. **Explicit restriction wins** — a session launched with `--tools` / `--exclude-tools` /
2748
- * `--no-builtin-tools` that is missing any of `MODE_REQUIRED_TOOL_NAMES` (pi's default four)
2749
- * stays as launched. Narrowing *further* would be the extension overriding a deliberate user
2750
- * instruction, and the PTC surface would be degraded anyway (bindings = `BUILTIN ∩ base`).
2751
- * `/ptc on` overrides this: the manual path enters with the restricted loadout as `base`, so
2752
- * bindings stay inside the user's allowlist.
2753
- */
2754
- function decideModeEntry(input) {
2755
- const { mode, defaultMode, active, manual } = input;
2756
- if (mode !== "tui") return {
2757
- enter: false,
2758
- reason: "not-tui"
2759
- };
2760
- if (!defaultMode && !manual) return {
2761
- enter: false,
2762
- reason: "config-off"
2763
- };
2764
- if (!PTC_MODE_TOOL_NAMES.some((name) => active.includes(name))) return {
2765
- enter: false,
2766
- reason: "tools-unavailable"
2767
- };
2768
- if (!manual) {
2769
- if (MODE_REQUIRED_TOOL_NAMES.filter((name) => !active.includes(name)).length > 0) return {
2770
- enter: false,
2771
- reason: "restricted-session"
2772
- };
2773
- }
2774
- return {
2775
- enter: true,
2776
- base: uniqueNames(active),
2777
- loadout: modeLoadout(active, input.hide ?? "builtins-only")
2778
- };
2779
- }
2780
- /**
2781
- * Tool names the binding table should be built from.
2782
- *
2783
- * While the mode is on this is the base snapshot (so the built-ins the mode just hid remain
2784
- * callable from inside a program); otherwise it is simply the live loadout, i.e. T7's rule.
2785
- */
2786
- function bindingSource(state, active) {
2787
- return state.enabled && state.base !== void 0 ? state.base : active;
2788
- }
2789
- /**
2790
- * Whether the live loadout is no longer the one this mode wrote — i.e. another extension (or the
2791
- * user) took ownership of the tool set. Always false while the mode is off.
2792
- */
2793
- function detectExternalLoadoutChange(state, active) {
2794
- if (!state.enabled || state.ourLoadout === void 0) return false;
2795
- return !sameToolSet(state.ourLoadout, active);
2796
- }
2797
- /**
2798
- * Reconstruct the base loadout on `session_start`.
2799
- *
2800
- * Two shapes are possible:
2801
- * - The session is still narrowed from a previous run of this mode (a `/reload`, or a resumed
2802
- * session): the live loadout is a subset of the PTC tools and the persisted record says the mode
2803
- * was on. The persisted `base` is then authoritative — but only to *undo our own hiding*.
2804
- * - Otherwise the live loadout is whatever this pi process was launched with, and it wins. That is
2805
- * what keeps a new `--tools …` choice from being overridden by an older session's snapshot.
2806
- */
2807
- function resolveBaseOnStart(persisted, active) {
2808
- const looksNarrowedByUs = active.length > 0 && active.every((name) => PTC_MODE_TOOL_NAMES.includes(name));
2809
- if (persisted?.enabled === true && looksNarrowedByUs && persisted.base !== void 0) return {
2810
- base: persisted.base,
2811
- restoreFirst: true
2812
- };
2813
- return {
2814
- base: active,
2815
- restoreFirst: false
2816
- };
2817
- }
2818
- /**
2819
- * Build the system-visible briefing for the mode.
2820
- *
2821
- * Generated from the *actual* binding list rather than hardcoded, because bindings vary by session
2822
- * (`base` is whatever the session was launched with) and a stale list would have the model calling
2823
- * names that are not bound.
2824
- */
2825
- function buildModeInstruction(bindings, hide) {
2826
- const builtinNote = hide === "all-but-ptc" ? "Every built-in tool is hidden from you, and so are other extensions' tools." : "The built-in tools (read, bash, edit, write, grep, find, ls) are hidden from you.";
2827
- const callable = bindings.map((name) => `tools.${name}(args)`);
2828
- const examples = bindings.slice(0, 2).map((name) => name === "read" ? "await tools.read({ path: \"src/index.ts\" })" : name === "bash" ? "await tools.bash({ command: \"pnpm test\" })" : `await tools.${name}({ /* … */ })`).join("\n");
2829
- return [
2830
- "[PTC MODE ACTIVE]",
2831
- "This session runs in PTC mode. " + builtinNote,
2832
- "",
2833
- "Work by writing one TypeScript program per task and running it:",
2834
- " - ptc_run_code for a plain program (its return value and console.log come back to you)",
2835
- " - ptc_workflow when the task has named phases and you want narration (log / phase /",
2836
- " parallel / pipeline are available there)",
2837
- "",
2838
- `The program body is an async function. Available inside it: ${callable.join(", ")}.`,
2839
- examples.length > 0 ? `Examples:\n${examples}` : "",
2840
- "",
2841
- "Batch a task's steps into ONE program instead of issuing many tool calls — gathering",
2842
- "intermediate output yourself and returning only what matters is the point of this mode.",
2843
- "Image-bearing tool results inside a program are attached to you after the run — never return",
2844
- "base64 image data as your program's value.",
2845
- "If the PTC surfaces cannot express what you need, say so and the user can run /ptc off."
2846
- ].filter((line) => line.length > 0).join("\n");
2847
- }
2848
- //#endregion
2849
- //#region src/mode/skills-section.ts
2850
- /**
2851
- * The system-prompt skills section, restored for PTC mode.
2852
- *
2853
- * pi advertises skills only when the session can actually open one:
2854
- *
2855
- * const skillFileReadTool = ["read", "bash"].find((tool) => selectedTools.includes(tool));
2856
- * if (skillFileReadTool && skills.length > 0) promptSections.skills = ...
2857
- * (pi-coding-agent: dist/core/system-prompt.js)
2858
- *
2859
- * PTC mode hides exactly those two tools, so that gate drops the section — and with it the model's
2860
- * only way to learn that skills exist. Measured on real session files: sessions before
2861
- * PTC-mode-by-default carried 43 skills in `SystemMessage.sections.skills`; PTC sessions carried
2862
- * no `skills` section at all. See ADR-0011.
2863
- *
2864
- * The list body stays pi's own — `formatSkillsForPrompt` is a public export and its XML shape is
2865
- * the agentskills.io spec, so re-deriving it here would only create drift. Only the loading
2866
- * sentence changes: "Use the read tool" is false in a session where `read` is hidden, and the way
2867
- * in is `tools.read` from inside a program.
2868
- */
2869
- /** The two tools pi accepts as "this session can read a skill file". */
2870
- const SKILL_READING_TOOL_NAMES = ["read", "bash"];
2871
- /** pi's sentence, which names a tool PTC mode hides. */
2872
- const PI_LOAD_INSTRUCTION = "Use the read tool to load a skill's file when the task matches its description.";
2873
- /** What replaces it: the same instruction, in the only call form this session has. */
2874
- const PTC_SKILL_LOAD_INSTRUCTION = "Use tools.read({ path }) inside a ptc_run_code program to load a skill's file when the task matches its description.";
2875
- /**
2876
- * Would pi withhold the skills section for this loadout?
2877
- *
2878
- * Mirrors the gate rather than guessing: it is true exactly when neither `read` nor `bash` is
2879
- * directly callable, which is the state the mode deliberately puts an ordinary session in.
2880
- */
2881
- function skillsSectionDropped(visibleTools) {
2882
- return !SKILL_READING_TOOL_NAMES.some((name) => visibleTools.includes(name));
2883
- }
2884
- /**
2885
- * Build the `skills` section body (pi wraps it in `<skills>…</skills>` itself).
2886
- *
2887
- * Returns `""` when nothing is advertisable — pi's formatter filters out skills marked
2888
- * `disable-model-invocation`, which is the correct behaviour to inherit: those are
2889
- * `/skill:name`-only by design.
2890
- *
2891
- * `format` is injectable so a test can drive the reworded-header branch without patching pi.
2892
- */
2893
- function buildPtcSkillsSection(skills, format = formatSkillsForPrompt) {
2894
- const section = format([...skills], "read").trim();
2895
- if (section.length === 0) return "";
2896
- if (section.includes(PI_LOAD_INSTRUCTION)) return section.replace(PI_LOAD_INSTRUCTION, PTC_SKILL_LOAD_INSTRUCTION);
2897
- return `${PTC_SKILL_LOAD_INSTRUCTION}\n\n${section}`;
2898
- }
2899
- //#endregion
2900
- //#region src/index.ts
2901
- /**
2902
- * pi-ptc-subagents — DSH PTC mode (Programmable Tool Calling) for pi.
2903
- *
2904
- * This module is the extension entrypoint pi loads (`package.json` → `pi.extensions`).
2905
- * The PTC machinery it exposes is complete and tested (T3): wire protocol, dispatcher,
2906
- * bindings and the two worker surfaces. On top of it sit the two model-facing tools:
2907
- *
2908
- * - `ptc_run_code` — bindings + Node + `console.log`, program's return value is the result
2909
- * - `ptc_workflow` — the same plus `log` / `phase` / `parallel` / `pipeline` and `args`
2910
- *
2911
- * There is no `agent()` helper on either surface (G1 #13 → decision B): calling it produces
2912
- * a plain `ReferenceError` through the normal code-run error path.
2913
- */
2914
- /** Footer status label shown while the mode is on. */
2915
- const MODE_STATUS_TEXT = "PTC";
2916
- /**
2917
- * Which tools the mode hides. The shipped extension always uses the default; it is a named
2918
- * constant so a future settings surface (or a test) has one place to change it.
2919
- */
2920
- const MODE_HIDE_STRATEGY = DEFAULT_HIDE_STRATEGY;
2921
- /**
2922
- * What to tell the user when the mode turns on. Names what became unreachable, because that is the
2923
- * part that surprises people — and the binding list, because that is the part they need in order
2924
- * to know what a program can still do.
2925
- */
2926
- function describeEntry(base, hide) {
2927
- return [
2928
- `PTC mode on — ${hide === "all-but-ptc" ? "all other tools" : "built-in tools (now reachable only as tools.<name> inside a program)"} hidden.`,
2929
- `Bindings available inside a program: ${resolveBindingNames(base).join(", ") || "(none)"}.`,
2930
- "Run /ptc off to restore the normal tool set."
2931
- ].join("\n");
2932
- }
2933
- function ptcSubagents(pi) {
2934
- const mode = initialModeState();
2935
- let briefingPending = false;
2936
- /**
2937
- * Per-turn worker pools (ADR-0017 §1–§2): one warm set per surface, created lazily by the
2938
- * first PTC run of the turn and retired at the turn's end. The holder is swapped rather
2939
- * than mutated so a `turn_end` drain can never race a run that is still holding a worker —
2940
- * the outgoing holder owns everything the ending turn created.
2941
- */
2942
- let turnPools = new TurnPools();
2943
- /**
2944
- * Bindings come from the mode's base snapshot while PTC mode is on, and from the live loadout
2945
- * otherwise (T7, #21). See `src/mode/ptc-mode.ts` for why the snapshot is required: the mode
2946
- * hides the built-ins, which would otherwise empty the binding table and make every
2947
- * `tools.<name>(...)` call fail.
2948
- */
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();
2956
- pi.registerTool(createPtcRunCodeTool({
2957
- getBindingSourceNames,
2958
- getPool: () => turnPools.get("run_code"),
2959
- depth: ptcDepth
2960
- }));
2961
- pi.registerTool(createPtcWorkflowTool({
2962
- getBindingSourceNames,
2963
- getPool: () => turnPools.get("workflow"),
2964
- depth: ptcDepth
2965
- }));
2966
- /**
2967
- * Retire the turn's pools. `drain()` terminates the warm workers, so nothing outlives the
2968
- * turn that spawned it; the next PTC run starts a fresh, lazily-created set. Idle workers are
2969
- * already `unref()`-ed by the pool, so a turn that ends without this hook firing can never
2970
- * keep the process alive either.
2971
- */
2972
- pi.on("turn_end", async () => {
2973
- const draining = turnPools;
2974
- turnPools = new TurnPools();
2975
- await draining.drain();
2976
- });
2977
- const paintStatus = (ctx) => {
2978
- ctx.ui.setStatus(PTC_MODE_STATUS_KEY, mode.enabled ? ctx.ui.theme.fg("accent", MODE_STATUS_TEXT) : void 0);
2979
- };
2980
- const enterMode = (base, loadout, ctx, announcement) => {
2981
- mode.enabled = true;
2982
- mode.base = base;
2983
- mode.ourLoadout = loadout;
2984
- pi.setActiveTools([...loadout]);
2985
- briefingPending = true;
2986
- pi.appendEntry(PTC_MODE_ENTRY_TYPE, {
2987
- enabled: true,
2988
- base: [...base]
2989
- });
2990
- paintStatus(ctx);
2991
- ctx.ui.notify(announcement, "info");
2992
- };
2993
- const exitMode = (ctx, opts) => {
2994
- const base = mode.base;
2995
- mode.enabled = false;
2996
- mode.base = void 0;
2997
- mode.ourLoadout = void 0;
2998
- briefingPending = false;
2999
- if (base !== void 0) pi.setActiveTools([...base]);
3000
- pi.appendEntry(opts.customType, { enabled: false });
3001
- paintStatus(ctx);
3002
- if (opts.announce) ctx.ui.notify("PTC mode off — built-in tools are directly callable again.", "info");
3003
- };
3004
- /** Restore the persisted mode record, if this session has one. */
3005
- const readPersistedMode = (ctx) => {
3006
- const entries = ctx.sessionManager.getEntries();
3007
- for (let index = entries.length - 1; index >= 0; index -= 1) {
3008
- const entry = entries[index];
3009
- if (entry?.type !== "custom" || entry.customType !== "ptc-mode") continue;
3010
- const data = entry.data;
3011
- const base = Array.isArray(data?.base) ? data.base.filter((name) => typeof name === "string") : void 0;
3012
- return {
3013
- enabled: data?.enabled === true,
3014
- ...base === void 0 ? {} : { base }
3015
- };
3016
- }
3017
- };
3018
- pi.registerCommand("ptc", {
3019
- description: "Show or toggle PTC mode (built-in tools reachable only from inside a PTC program)",
3020
- handler: async (args, ctx) => {
3021
- const action = args.trim().toLowerCase();
3022
- if (action === "off") {
3023
- if (!mode.enabled) {
3024
- ctx.ui.notify("PTC mode is already off.", "info");
3025
- return;
3026
- }
3027
- exitMode(ctx, {
3028
- announce: true,
3029
- customType: PTC_MODE_ENTRY_TYPE
3030
- });
3031
- return;
3032
- }
3033
- if (action !== "on") {
3034
- ctx.ui.notify(mode.enabled ? `PTC mode is ON — run /ptc off to restore built-in tools.\nBindings: ${resolveBindingNames(mode.base).join(", ") || "(none)"}` : "PTC mode is OFF — run /ptc on to enter it.", "info");
3035
- return;
3036
- }
3037
- if (mode.enabled) {
3038
- ctx.ui.notify("PTC mode is already on.", "info");
3039
- return;
3040
- }
3041
- const decision = decideModeEntry({
3042
- mode: ctx.mode,
3043
- defaultMode: true,
3044
- active: pi.getActiveTools(),
3045
- manual: true,
3046
- hide: MODE_HIDE_STRATEGY
3047
- });
3048
- if (!decision.enter) {
3049
- ctx.ui.notify(decision.reason === "tools-unavailable" ? "Cannot enter PTC mode: ptc_run_code / ptc_workflow are not active in this session." : "Cannot enter PTC mode in this run mode.", "warning");
3050
- return;
3051
- }
3052
- enterMode(decision.base, decision.loadout, ctx, describeEntry(decision.base, MODE_HIDE_STRATEGY));
3053
- }
3054
- });
3055
- pi.on("session_start", async (_event, ctx) => {
3056
- const config = readDefaultModeConfig(getAgentDir());
3057
- if (config.error !== void 0) ctx.ui.notify(`pi-ptc-subagents: ${config.error}`, "warning");
3058
- const persisted = readPersistedMode(ctx);
3059
- const { base, restoreFirst } = resolveBaseOnStart(persisted, pi.getActiveTools());
3060
- if (restoreFirst) pi.setActiveTools([...base]);
3061
- const decision = decideModeEntry({
3062
- mode: ctx.mode,
3063
- defaultMode: config.defaultMode,
3064
- active: base,
3065
- manual: false,
3066
- hide: MODE_HIDE_STRATEGY
3067
- });
3068
- const wantMode = persisted?.enabled ?? decision.enter;
3069
- if (decision.enter && wantMode) {
3070
- enterMode(base, decision.loadout, ctx, describeEntry(base, MODE_HIDE_STRATEGY));
3071
- return;
3072
- }
3073
- paintStatus(ctx);
3074
- if (!decision.enter && decision.reason !== "not-tui" && persisted === void 0) {
3075
- if (decision.reason === "restricted-session") ctx.ui.notify("PTC mode skipped: this session was launched with an explicit tool restriction.", "info");
3076
- }
3077
- });
3078
- /**
3079
- * Put back the skills section pi withholds while the mode hides `read`/`bash` (ADR-0011).
3080
- *
3081
- * pi advertises skills only when the session can open a skill file at all (`read` or `bash`
3082
- * among the selected tools), so a PTC session silently loses the whole `<available_skills>`
3083
- * list: the model stops knowing skills exist. Writing `sections.skills` here is the seam pi
3084
- * leaves open — custom sections are merged after its own, so this one wins.
3085
- *
3086
- * The same options object is reused across turns and sessions of a resume, so the injection is
3087
- * undone the moment the mode stops hiding those tools: a `/ptc off` must not leave PTC-flavoured
3088
- * loading instructions behind while `read` is directly callable again.
3089
- */
3090
- let skillsSectionInjected = false;
3091
- const syncSkillsSection = (options, hidden) => {
3092
- if (options === void 0) return;
3093
- if (!mode.enabled || !hidden) {
3094
- if (skillsSectionInjected) {
3095
- delete options.sections.skills;
3096
- skillsSectionInjected = false;
3097
- }
3098
- return;
3099
- }
3100
- const section = buildPtcSkillsSection(options.skills);
3101
- if (section.length === 0) return;
3102
- options.sections.skills = section;
3103
- skillsSectionInjected = true;
3104
- };
3105
- pi.on("before_agent_start", async (event, ctx) => {
3106
- const options = event.systemPromptOptions;
3107
- syncSkillsSection(options, skillsSectionDropped(pi.getActiveTools()));
3108
- if (!mode.enabled) return;
3109
- if (detectExternalLoadoutChange(mode, pi.getActiveTools())) {
3110
- exitMode(ctx, {
3111
- announce: false,
3112
- customType: PTC_MODE_ENTRY_TYPE
3113
- });
3114
- syncSkillsSection(options, skillsSectionDropped(pi.getActiveTools()));
3115
- ctx.ui.notify("PTC mode off — another extension changed the active tool set.", "warning");
3116
- return;
3117
- }
3118
- if (!briefingPending) return;
3119
- briefingPending = false;
3120
- return { message: {
3121
- customType: "ptc-mode-briefing",
3122
- content: buildModeInstruction(resolveBindingNames(mode.base), MODE_HIDE_STRATEGY),
3123
- display: false
3124
- } };
3125
- });
3126
- }
3127
- //#endregion
3128
- export { BUILTIN_BINDING_NAMES, DEFAULT_BINDING_NAMES, DEFAULT_CONFIG, DEFAULT_HIDE_STRATEGY, HOST_FRAME_KIND, MAX_LINE_CHARS, MODE_REQUIRED_TOOL_NAMES, PTC_ERROR_KIND, PTC_LOG_LEVEL, PTC_MODE_CONFIG_FILE, PTC_MODE_ENTRY_TYPE, PTC_MODE_STATUS_KEY, PTC_MODE_TOOL_NAMES, PTC_SKILL_LOAD_INSTRUCTION, SKILL_READING_TOOL_NAMES, TurnPools, WORKER_ENV_ALLOW_LIST, WORKER_FRAME_KIND, WorkerPool, bindingSource, buildModeInstruction, buildPtcSkillsSection, createBuiltinBindings, createWorkerEnv, decideModeEntry, ptcSubagents as default, detectExternalLoadoutChange, effectiveTimeoutMs, initialModeState, modeLoadout, readDefaultModeConfig, renderModelValue, resolveBaseOnStart, resolveConfig, runPtcProgram, sameToolSet, sanitizeText, skillsSectionDropped, stripAnsi };
1
+ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./protocol-CfOgxz3u.js";import{DEFAULT_MAX_BYTES as s,DEFAULT_MAX_LINES as c,createBashTool as l,createEditTool as u,createFindTool as d,createGrepTool as f,createLsTool as p,createReadTool as m,createWriteTool as h,defineTool as g,formatSize as _,formatSkillsForPrompt as v,getAgentDir as y,truncateTail as b}from"@earendil-works/pi-coding-agent";import{Type as x}from"typebox";import{validateToolArguments as S}from"@earendil-works/pi-ai";import{spawn as ee}from"node:child_process";import*as C from"node:fs";import{existsSync as w,readFileSync as T,writeFileSync as E}from"node:fs";import*as D from"node:os";import{tmpdir as te}from"node:os";import*as O from"node:path";import{dirname as k,join as A,resolve as ne}from"node:path";import{randomUUID as j}from"node:crypto";import{MessageChannel as re,Worker as M}from"node:worker_threads";import N from"node:diagnostics_channel";import{fileURLToPath as P,pathToFileURL as ie}from"node:url";import{truncateToWidth as ae,visibleWidth as F}from"@earendil-works/pi-tui";function oe(e,t,n){let r=[`<pi-ptc-context depth="`+t+`" max-depth="`+n+`">`,`You are a PTC run at depth `+t+` (root is depth 0). You may write PTC programs and you may`,`call pi.dispatch(...) to spawn further children, but each level costs a fresh pi subprocess.`,`The remaining depth budget is `+n+` - `+t+`. Beyond it, pi.dispatch rejects with`,`{ status: "rejected", errorMessage: "dispatch depth limit reached" }.`,`</pi-ptc-context>`].join(`
2
+ `);return e.length===0?r:e+`
3129
3
 
4
+ `+r}function se(){return{text:``,status:`rejected`,started:!1,agentName:`unknown`,durationMs:0,exitCode:-1,errorMessage:`dispatch depth limit reached`}}function ce(){return{text:``,status:`rejected`,started:!1,agentName:`unknown`,durationMs:0,exitCode:-1,errorMessage:`dispatch concurrency limit reached`}}function le(e){return e.aborted?{status:`rejected`,errorMessage:`dispatch cancelled`}:e.exitCode===0&&e.finalText.length>0?{status:`fulfilled`}:e.exitCode===0?{status:`rejected`,errorMessage:`dispatch produced no final text`}:{status:`rejected`}}function ue(e,t){if(e.pid===void 0)return!1;try{return e.kill(t),!0}catch{return!1}}function I(e,t){let n=e.match(RegExp(`^`+t+`:\\s*(.+)$`,`m`));if(!n)return;let r=(n[1]??``).trim();if(r.length>=2){let e=r[0],t=r[r.length-1];(e===`"`&&t===`"`||e===`'`&&t===`'`)&&(r=r.slice(1,-1))}return r.length===0?void 0:r}function de(e,t){let n=e.match(RegExp(`^`+t+`:\\s*(.+)$`,`m`));if(!n)return;let r=(n[1]??``).trim();r.startsWith(`[`)&&r.endsWith(`]`)&&(r=r.slice(1,-1));let i=r.split(`,`).map(e=>e.trim()).filter(e=>e.length>0);return i.length===0?void 0:i}function fe(e){let t=e.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);if(!t)return null;let n=t[1]??``,r=t[2]??``,i=I(n,`name`);if(!i)return null;let a=I(n,`description`)??``,o=I(n,`model`)??``,s=de(n,`tools`),c={name:i,systemPrompt:r.trim()};return a!==void 0&&(c.description=a),o!==void 0&&(c.model=o),s!==void 0&&(c.tools=s),c}function pe(e,t,n){let r=O.join(D.homedir(),`.pi`,`agent`,`agents`),i=O.join(t,`.pi`,`agents`),a=[];(n===`user`||n===`both`)&&a.push({path:O.join(r,e+`.md`),source:`user`}),(n===`project`||n===`both`)&&a.push({path:O.join(i,e+`.md`),source:`project`});for(let t of a){let n;try{n=C.readFileSync(t.path,`utf-8`)}catch{continue}let r=fe(n);if(r&&r.name===e){let e={name:r.name,source:t.source,systemPrompt:r.systemPrompt};return r.model!==void 0&&(e.model=r.model),r.tools!==void 0&&(e.tools=r.tools),e}}return null}function me(e,t,n){let r=[`--mode`,`json`,`-p`,`--no-session`],i=e.model??t.model;i&&r.push(`--model`,i);let a=e.thinkingLevel??`off`;return r.push(`--thinking`,a),t.tools&&t.tools.length>0&&r.push(`--tools`,t.tools.join(`,`)),r.push(`--append-system-prompt`,n),r.push(`Task: `+e.task),r}function he(e){let t=e.trim();if(t.length===0)return null;try{return JSON.parse(t)}catch{return null}}async function ge(e,t){let n=await C.promises.mkdtemp(O.join(D.tmpdir(),`pi-dispatch-`)),r=e.replace(/[^\w.-]+/g,`_`),i=O.join(n,`prompt-`+r+`.md`);return await C.promises.writeFile(i,t,{encoding:`utf-8`,mode:384}),{dir:n,filePath:i}}function _e(e){try{C.unlinkSync(e.filePath)}catch{}try{C.rmdirSync(e.dir)}catch{}}async function ve(e,t){let n=t.depth+1;if(n>t.maxDispatchDepth)return se();let r=e.cwd??t.cwd,i=e.agentScope??`user`,a=Date.now(),o=pe(e.agent,r,i);if(!o)return{text:``,status:`rejected`,started:!1,agentName:e.agent,durationMs:Date.now()-a,exitCode:1,errorMessage:`unknown agent: `+e.agent+` (agentScope=`+i+`, cwd=`+r+`)`};let s=oe(o.systemPrompt,n,t.maxDispatchDepth),c=await ge(o.name,s),l=me(e,o,c.filePath);return await new Promise(e=>{let i=``,s=``,u=``,d,f=-1,p=!1,m,h,g=!1,_=(n,r,i=!0)=>{if(p)return;p=!0,m&&clearTimeout(m),t.signal&&t.signal.removeEventListener(`abort`,v),_e(c);let l={text:u,status:n,started:i,agentName:o.name,durationMs:Date.now()-a,exitCode:f};d&&(l.usage=d),s.length>0&&(l.stderr=s),r&&(l.errorMessage=r),e(l)},v=()=>{!h||h.killed||p||(g=!0,ue(h,`SIGTERM`),m=setTimeout(()=>{h&&!h.killed&&ue(h,`SIGKILL`)},5e3))};try{h=ee(`pi`,l,{cwd:r,shell:!1,stdio:[`ignore`,`pipe`,`pipe`],env:{...process.env,PI_PTC_DEPTH:String(n)}})}catch(e){_(`rejected`,`failed to spawn pi: `+(e instanceof Error?e.message:String(e)),!1);return}t.signal&&(t.signal.aborted?v():t.signal.addEventListener(`abort`,v,{once:!0}));let y=h.stdout;if(!y)return;y.on(`data`,e=>{i+=e.toString(`utf-8`);let t=i.indexOf(`
5
+ `);for(;t>=0;){let e=i.slice(0,t);i=i.slice(t+1);let n=he(e);if(!n){t=i.indexOf(`
6
+ `);continue}if(n.type===`message_end`&&n.message&&n.message.role===`assistant`){let e=n.message;if(e.usage){let t=d??{input:0,output:0,cacheRead:0,cacheWrite:0,cost:0,turns:0};t.input+=e.usage.input??0,t.output+=e.usage.output??0,t.cacheRead+=e.usage.cacheRead??0,t.cacheWrite+=e.usage.cacheWrite??0,t.cost+=e.usage.cost?.total??0,t.turns+=1,d=t}if(Array.isArray(e.content))for(let t of e.content)t&&t.type===`text`&&typeof t.text==`string`&&(u=t.text)}t=i.indexOf(`
7
+ `)}});let b=h.stderr;b&&(b.on(`data`,e=>{s+=e.toString(`utf-8`)}),h.on(`close`,e=>{f=e??-1;let{status:t,errorMessage:n}=le({exitCode:f,finalText:u,aborted:g});_(t,n)}),h.on(`error`,e=>{s+=`[spawn-error] `+e.message+`
8
+ `,_(`rejected`,`failed to spawn pi: `+e.message,!1)}))})}const L=[`read`,`bash`,`edit`,`write`,`grep`,`find`,`ls`],ye=`pi.dispatch`,be=L,xe={read:m,bash:l,edit:u,write:h,grep:f,find:d,ls:p};function Se(e){let t=e.names??be,n=e.includeDispatch??e.names===void 0,r=/* @__PURE__ */ new Map;for(let n of t){let t=xe[n];if(!t)throw TypeError(`unknown PTC binding "${n}"; known bindings: ${L.join(`, `)}`);let i=t(e.cwd);r.set(n,{name:n,execute:async(e,t)=>{let n=`ptc:${t.callId}`,r={type:`toolCall`,id:n,name:i.name,arguments:e},a=S(i,r),o=await i.execute(n,a,t.signal,void 0);return{content:o.content,details:o.details===void 0?null:o.details}}})}return n&&r.set(ye,{name:ye,execute:async(t,n)=>ve(t,{signal:n.signal,callId:n.callId,cwd:e.cwd,depth:n.depth,maxDispatchDepth:n.maxDispatchDepth})}),r}const R=Object.freeze({timeoutMs:12e4,maxTimeoutMs:6e5,maxOutputBytes:67108864,maxMessageBytes:134217728,maxPendingCalls:128,maxParallelSubCalls:10,dispatchConcurrency:8,maxDispatchDepth:3,maxItemsPerCall:4096,graceMs:3e3,maxOldGenerationSizeMb:512,maxYoungGenerationSizeMb:64,poolSize:4,poolAcquireTimeoutMs:3e4,drainGraceMs:5e3}),Ce=Object.freeze([`PATH`,`PATHEXT`,`SYSTEMROOT`,`WINDIR`,`TEMP`,`TMP`]);function we(e=process.env){let t={};for(let n of Ce){let r=e[n];typeof r==`string`&&r.length>0&&(t[n]=r)}return t}function Te(e={}){let t={...R};for(let n of Object.keys(R)){let r=e[n];if(r!==void 0){if(typeof r!=`number`||!Number.isFinite(r)||r<=0)throw TypeError(`invalid PTC config override for ${n}: ${String(r)}`);t[n]=r}}return t}function Ee(e,t=R){return e===void 0||!Number.isFinite(e)||e<=0?t.timeoutMs:Math.min(e,t.maxTimeoutMs)}function De(){let e=/* @__PURE__ */ new Map;return{recordStart(t,n,r){if(e.has(t))throw Error(`duplicate sub-call callId ${t}`);e.set(t,{callId:t,name:n,args:r,status:`running`,startMs:Date.now()})},recordEnd(t,n,r){let i=e.get(t);if(i===void 0)throw Error(`recordEnd on unknown callId ${t}`);return i.status=n,i.endMs=Date.now(),i.durationMs=i.endMs-i.startMs,n===`rejected`&&(i.durationMs=0),r?.errorMessage!==void 0&&(i.errorMessage=r.errorMessage),r?.resultSummary!==void 0&&(i.resultSummary=r.resultSummary),i},snapshot(){return[...e.values()].map(e=>({...e}))}}}function Oe(e){return{name:`ptc-${e.surface}`,env:e.env,workerData:e.runId===void 0?{env:e.env}:{runId:e.runId,env:e.env},resourceLimits:{maxOldGenerationSizeMb:e.limits.maxOldGenerationSizeMb,maxYoungGenerationSizeMb:e.limits.maxYoungGenerationSizeMb}}}const z={acquireLatency:N.channel(`ptc:pool:acquire-latency`),resetTime:N.channel(`ptc:worker:reset-time`),imageBytes:N.channel(`ptc:image:hoist-bytes`)};function B(e,t){e.publish(t)}var ke=class{#e;#t;#n;#r;#i;#a=[];#o=/* @__PURE__ */ new Set;#s=[];#c=/* @__PURE__ */ new WeakMap;#l=0;#u=0;#d=!1;constructor(e){if(typeof e.buildWorkerUrl!=`function`)throw TypeError(`WorkerPool: buildWorkerUrl must be a function`);if(e.size!==void 0&&(typeof e.size!=`number`||!Number.isFinite(e.size)||e.size<=0))throw TypeError(`WorkerPool: size must be a positive finite number, received ${String(e.size)}`);if(e.acquireTimeoutMs!==void 0&&(typeof e.acquireTimeoutMs!=`number`||!Number.isFinite(e.acquireTimeoutMs)||e.acquireTimeoutMs<=0))throw TypeError(`WorkerPool: acquireTimeoutMs must be a positive finite number, received ${String(e.acquireTimeoutMs)}`);if(e.drainGraceMs!==void 0&&(typeof e.drainGraceMs!=`number`||!Number.isFinite(e.drainGraceMs)||e.drainGraceMs<=0))throw TypeError(`WorkerPool: drainGraceMs must be a positive finite number, received ${String(e.drainGraceMs)}`);this.#e=e.buildWorkerUrl,this.#t=e.size??4,this.#n=e.acquireTimeoutMs??3e4,this.#r=e.drainGraceMs??5e3,this.#i=e.workerOptions}async acquire(){if(this.#d)throw Error(`pool has been drained and can no longer serve workers`);this.#l+=1;let e=Date.now(),t=this.#a.shift();for(;t!==void 0&&!this.#p(t);)t=this.#a.shift();if(t!==void 0)return this.#o.add(t),t.ref(),B(z.acquireLatency,{poolSize:this.#t,waiters:this.#s.length,durationMs:Date.now()-e}),t;if(this.#o.size<this.#t){let t=this.#f();return this.#o.add(t),B(z.acquireLatency,{poolSize:this.#t,waiters:this.#s.length,durationMs:Date.now()-e}),t}return this.#u+=1,await new Promise((t,n)=>{let r=setTimeout(()=>{let e=this.#s.findIndex(e=>e.timer===r);e>=0&&this.#s.splice(e,1),n(/* @__PURE__ */ Error(`pool acquire timed out after ${this.#n} ms`))},this.#n);this.#s.push({resolve:t,reject:n,timer:r,startedAt:e})})}release(e){if(!this.#o.delete(e)||!this.#p(e))return;this.#c.set(e,Date.now());let t=this.#s.shift();if(t){clearTimeout(t.timer),this.#o.add(e),B(z.acquireLatency,{poolSize:this.#t,waiters:this.#s.length,durationMs:Date.now()-t.startedAt}),t.resolve(e);return}this.#a.push(e),e.unref()}lastSettledAt(e){return this.#c.get(e)}async drain(){for(;this.#s.length>0;){let e=this.#s.shift();if(!e)break;clearTimeout(e.timer),e.reject(/* @__PURE__ */ Error(`pool drained before acquire could be satisfied`))}let e=Date.now()+this.#r;for(;this.#o.size>0&&Date.now()<e;)await new Promise(e=>setTimeout(e,5));let t=[...this.#a,...this.#o];this.#a.length=0,this.#o.clear(),await Promise.all(t.map(e=>e.terminate().catch(()=>void 0))),this.#d=!0}stats(){return{resident:this.#o.size+this.#a.length,inFlight:this.#o.size,waiters:this.#s.length,totalAcquires:this.#l,poolExhaustions:this.#u}}#f(){let e=new M(this.#e(),this.#i);return e.once(`exit`,()=>{let t=this.#a.indexOf(e);if(t>=0){this.#a.splice(t,1);return}this.#o.delete(e)}),e}#p(e){return e.threadId!==-1}};function Ae(e){B(z.imageBytes,{byteLength:e})}function je(e){B(z.resetTime,{durationMs:e})}function Me(){let e=k(P(import.meta.url)),t=[ne(e,`worker.js`),ne(e,`worker-entry.ts`)];for(let e of t)if(w(e))return ie(e);throw Error(`worker entry not found; tried: ${t.join(`, `)}`)}function Ne(e){if(typeof e!=`object`||!e)return;let t=e;if(t.status!==`rejected`)return;let n=typeof t.errorMessage==`string`?t.errorMessage:t.started===!1?`dispatch refused`:`dispatch failed`;return{kind:t.started===!1?`refused`:`failed`,message:n}}function V(e){return e instanceof Error?e.message:String(e)}function Pe(e){if(typeof e!=`object`||!e)return;let t=e.content;if(Array.isArray(t)&&t.length>0){let e=t[0];if(typeof e==`object`&&e){let t=e.text;if(typeof t==`string`&&t.length>0){let e=t.indexOf(`
9
+ `);return e===-1?t:t.slice(0,e)}}}}function Fe(e){return e instanceof ArrayBuffer||ArrayBuffer.isView(e)}function Ie(e,t){let n=/* @__PURE__ */ new WeakSet,r=e=>{if(e!=null){if(e instanceof ArrayBuffer){t(e);return}if(ArrayBuffer.isView(e)){t(e);return}if(typeof e==`object`&&!n.has(e)){if(n.add(e),Array.isArray(e)){for(let t of e)r(t);return}for(let t of Object.keys(e))r(e[t])}}};r(e)}function Le(e){try{let t=Buffer.byteLength(JSON.stringify(e,(e,t)=>Fe(t)?null:t)??``,`utf8`),n=0;return Ie(e,e=>{n+=e.byteLength}),t+n}catch{return 1/0}}async function Re(e){let i=Te(e.config),s=we(),c=e.runId??j(),l=Ee(e.timeoutMs,i),u=[],d=[],f=[];if(e.signal?.aborted)return{logs:u,narrations:d,phases:f,error:{kind:n.abort,message:`run cancelled before start`}};let p;try{p=e.pool?await e.pool.acquire():new M(Me(),Oe({surface:e.surface,env:s,limits:i,runId:c}))}catch(e){return{logs:u,narrations:d,phases:f,error:{kind:n.workerExit,message:`pool acquire failed: ${V(e)}`}}}return await new Promise(s=>{let m=new re,h=m.port1,g=m.port2,_=new AbortController,v=!1,y=De(),b=()=>{v||e.onSubCallChange?.(()=>y.snapshot())},x={recordStart(e,t,n){y.recordStart(e,t,n),b()},recordEnd(e,t,n){let r=y.recordEnd(e,t,n);return b(),r},snapshot:()=>y.snapshot()},S,ee=!1,C=0,w=0,T=0,E=0,D=[],te=e.depth??0,O,k,A=[],ne=e=>{let t=[],n=e?.content;if(!Array.isArray(n))return t;for(let e of n){if(typeof e!=`object`||!e)continue;let n=e;if(n.type!==`image`)continue;let r;typeof n.data==`string`&&n.data.length>0?r=n.data:n.bytes instanceof ArrayBuffer&&n.bytes.byteLength>0&&(r=Buffer.from(n.bytes).toString(`base64`)),r!==void 0&&t.push({data:r,mimeType:typeof n.mimeType==`string`?n.mimeType:`application/octet-stream`})}return t},j=e=>e===`timeout`?`run timed out after ${l} ms`:`run cancelled`,M=e=>e===`timeout`?n.timeout:n.abort,N=t=>{if(v)return;v=!0,O&&clearTimeout(O),k&&clearTimeout(k),e.signal?.removeEventListener(`abort`,pe),_.abort(),h.close(),e.pool?e.pool.release(p):p.terminate();let n=x.snapshot();s(n.length>0?{...t,subCalls:n}:t)},P=(e,t,n)=>{N({logs:u,narrations:d,phases:f,error:n===void 0?{kind:e,message:t}:{kind:e,message:t,stack:n}})},ie=e=>{if(!v){if(!r(e)){P(n.protocol,`internal error: malformed host frame`);return}h.postMessage(e)}},ae=()=>{let t={kind:o.init,runId:c,surface:e.surface,code:e.code,bindings:[...e.bindings.keys()],bindingCandidates:[...L],maxItemsPerCall:i.maxItemsPerCall,maxPendingCalls:i.maxPendingCalls,...e.args===void 0?{}:{args:e.args}};if(!r(t)){P(n.protocol,`internal error: malformed init frame`);return}let a=Le(t);if(a>i.maxMessageBytes){P(n.protocol,`init frame of ${a} bytes exceeds maxMessageBytes (${i.maxMessageBytes})`);return}ie(t)},F=(e,t)=>(w+=e,w<=i.maxOutputBytes||(P(n.outputLimit,`${t} exceeded the output budget: logs + result reached ${w} bytes (maxOutputBytes=${i.maxOutputBytes}); ${u.length} log line(s) retained`),!1)),oe=async()=>{if(E<i.maxParallelSubCalls){E+=1;return}await new Promise(e=>{D.push(e)})},se=()=>{let e=D.shift();if(e){e();return}--E},le=e=>{if(x.recordStart(e.callId,e.tool,e.args),C+=1,C>i.maxPendingCalls){x.recordEnd(e.callId,`error`,{errorMessage:`more than maxPendingCalls (${i.maxPendingCalls}) binding calls in flight`}),P(n.protocol,`more than maxPendingCalls (${i.maxPendingCalls}) binding calls in flight`);return}ue(e).finally(()=>{--C})},ue=async t=>{let r=e.bindings.get(t.tool);if(!r){x.recordEnd(t.callId,`error`,{errorMessage:`no binding named "${t.tool}" in this run; available bindings: `+([...e.bindings.keys()].join(`, `)||`(none)`)}),I({kind:o.callResult,callId:t.callId,tool:t.tool,ok:!1,message:`no binding named "${t.tool}" in this run; available bindings: `+([...e.bindings.keys()].join(`, `)||`(none)`)});return}let a=t.tool===ye;if(a){if(T>=i.dispatchConcurrency){x.recordEnd(t.callId,`rejected`,{errorMessage:`dispatch concurrency limit reached`}),I({kind:o.callResult,callId:t.callId,tool:t.tool,ok:!0,value:ce()});return}T+=1}else if(await oe(),v){x.recordEnd(t.callId,`cancelled`,{errorMessage:`run settled before binding started`}),se();return}try{let e=await r.execute(t.args,{signal:_.signal,callId:t.callId,depth:te,maxDispatchDepth:i.maxDispatchDepth}),s=ne(e),c={kind:o.callResult,callId:t.callId,tool:t.tool,ok:!0,value:e},l=Le(c);if(l>i.maxMessageBytes){x.recordEnd(t.callId,`error`,{errorMessage:`callResult frame of ${l} bytes exceeds maxMessageBytes (${i.maxMessageBytes})`}),P(n.protocol,`callResult frame of ${l} bytes exceeds maxMessageBytes (${i.maxMessageBytes})`);return}if(I(c))for(let e of s)A.push(e),Ae(Buffer.byteLength(e.data,`base64`));if(_.signal.aborted)x.recordEnd(t.callId,`cancelled`,{errorMessage:`binding aborted by run cancellation`});else{let n=a?Ne(e):void 0;n===void 0?x.recordEnd(t.callId,`ok`,{resultSummary:Pe(e)}):x.recordEnd(t.callId,n.kind===`refused`?`rejected`:`error`,{errorMessage:n.message})}}catch(e){_.signal.aborted?x.recordEnd(t.callId,`cancelled`,{errorMessage:V(e)}):x.recordEnd(t.callId,`error`,{errorMessage:V(e)}),I({kind:o.callResult,callId:t.callId,tool:t.tool,ok:!1,message:V(e)})}finally{a?--T:se()}},I=e=>{if(v)return!1;try{return h.postMessage(e),!0}catch(t){try{h.postMessage({kind:o.callResult,callId:e.callId,tool:e.tool,ok:!1,message:`binding result could not be transferred: ${V(t)}`})}catch{P(n.protocol,`binding result is not transferable`)}return!1}},de=e=>S?{logs:u,narrations:d,phases:f,error:{kind:M(S),message:j(S)}}:e,fe=r=>{if(v)return;if(!a(r)){P(n.protocol,`malformed worker frame`);return}let o=r.kind===t.ready?0:Le(r);if(!Number.isFinite(o)){P(n.protocol,`worker frame could not be serialized for accounting`);return}if(o>i.maxMessageBytes){P(n.protocol,`control frame of ${o} bytes exceeds maxMessageBytes (${i.maxMessageBytes})`);return}switch(r.kind){case t.ready:if(e.pool){let t=e.pool.lastSettledAt(p);t!==void 0&&je(Date.now()-t)}if(!ee&&(ee=!0,S)){me(S);return}ae();return;case t.call:le(r);return;case t.log:F(o,`console output`)&&u.push(r.text);return;case t.narration:F(o,`workflow narration`)&&d.push(r.message);return;case t.phase:F(o,`workflow phase`)&&f.push(r.title);return;case t.result:{if(!F(o,`completion value`))return;let e={logs:u,narrations:d,phases:f,...A.length>0?{images:A}:{}};N(de(r.value===void 0?e:{...e,value:r.value}));return}case t.error:N(de({logs:u,narrations:d,phases:f,error:r.error}));return;default:P(n.protocol,`unknown worker frame`)}};function pe(){he(`abort`)}let me=e=>{k&&clearTimeout(k),k=setTimeout(()=>{P(M(e),j(e))},i.graceMs)};function he(e){if(!v){if(S){P(M(S),j(S));return}S=e,_.abort(),ie({kind:o.cancel,reason:e}),me(e)}}p.on(`error`,e=>{P(n.workerExit,`worker failed: ${V(e)}`)}),p.on(`exit`,e=>{P(n.workerExit,`worker exited with code ${e} before the run completed`)}),h.on(`message`,fe),h.on(`messageerror`,()=>{P(n.protocol,`worker sent a frame that could not be deserialized`)}),e.signal?.addEventListener(`abort`,pe,{once:!0}),O=setTimeout(()=>{he(`timeout`)},l);let ge={kind:o.connect,port:g};r(ge)?p.postMessage(ge,[g]):P(n.protocol,`internal error: malformed connect frame`)})}const ze=200,Be=new RegExp([`(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))`,`[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]`].join(`|`),`g`);function Ve(e){return!e.includes(`\x1B`)&&!e.includes(`›`)?e:e.replace(Be,``)}function H(e){let t=Ve(e).replace(/\r/g,``);if(t.length===0)return t;let n=``;for(let e of t){let t=e.codePointAt(0);if(t!==void 0){if(t===10||t===9){n+=e;continue}t<=31||t>=65529&&t<=65531||(n+=e)}}return n}const He=/^[A-Za-z_$][A-Za-z0-9_$]*$/;function U(e){return He.test(e)?e:JSON.stringify(e)}function Ue(e){if(e===null)return`null`;if(typeof e==`number`||typeof e==`boolean`)return String(e);if(typeof e==`string`){let t=H(e);return t.includes(`
10
+ `)?void 0:JSON.stringify(t)}if(Array.isArray(e)){let t=[];for(let n of e){let e=Ue(n);if(e===void 0)return;t.push(e)}return`[${t.join(`, `)}]`}let t=Object.keys(e);if(t.length===0)return`{}`;let n=[];for(let r of t){let t=Ue(e[r]??null);if(t===void 0)return;n.push(`${U(r)}: ${t}`)}return`{${n.join(`, `)}}`}function We(e){let t=Ue(e);if(t!==void 0)return t.length<=100?t:void 0}function Ge(e){return e===null?`null`:typeof e==`string`?H(e):String(e)}function Ke(e,t){if(typeof e!=`object`||!e)return[`${t}${Ge(e)}`];if(Array.isArray(e)){if(e.length===0)return[`${t}[]`];let n=[`${t}[`];for(let r of e){let e=We(r);if(e!==void 0){n.push(`${t} ${e}`);continue}n.push(...qe(r,t+` `))}return n.push(`${t}]`),n}let n=Object.keys(e);if(n.length===0)return[`${t}{}`];let r=[`${t}{`],i=t+` `;for(let t of n){let n=e[t]??null,a=We(n);if(a!==void 0){r.push(`${i}${U(t)}: ${a}`);continue}if(typeof n==`string`){r.push(`${i}${U(t)}:`);for(let e of H(n).split(`
11
+ `))r.push(e.length===0?``:`${i} ${e}`);continue}let o=Ke(n,i);r.push(`${i}${U(t)}: ${(o[0]??``).trimStart()}`.trimEnd(),...o.slice(1))}return r.push(`${t}}`),r}function qe(e,t){return typeof e==`string`?H(e).split(`
12
+ `):typeof e!=`object`||!e?[String(e)]:Ke(e,t)}function Je(e){return e.split(`
13
+ `).map(e=>e.length>200?`${e.slice(0,199)}…`:e).join(`
14
+ `)}function Ye(e){if(typeof e==`string`)return Je(H(e));if(typeof e!=`object`||!e)return String(e);let t=We(e);return Je((t===void 0?qe(e,``):[t]).join(`
15
+ `))}function Xe(e=process.env){let t=e.PI_PTC_DEPTH;return t===void 0||!/^\d+$/.test(t)?0:Number.parseInt(t,10)}function W(e){if(e===void 0)return L;let t=new Set(e);return L.filter(e=>t.has(e))}const Ze=["Image-bearing tool results inside a program (a `tools.read` on a PNG, say) are attached to you after the run — never return image data as the completion value.",`Use ptc_run_code when a task needs several tool calls whose intermediate output you do not need to see — gather it inside the program and return only the final value.`,`Use ptc_workflow instead of ptc_run_code when the work has named phases and the user benefits from seeing progress narration.`],Qe={run_code:`ptc_run_code`,workflow:`ptc_workflow`};function $e(e){let t=e?.cwd;return typeof t==`string`&&t.length>0?t:process.cwd()}function et(e){let{surface:t,startedAt:n,onUpdate:r}=e;if(r===void 0)return{update:()=>{},cancel:()=>{}};let i=0,a,o,s=()=>{if(o===void 0)return;let e=o;o=void 0,i=Date.now(),r({content:[],details:{surface:t,logs:[],narrations:[],phases:[],warnings:[],durationMs:Date.now()-n,imageCount:0,subCalls:e()}})};return{update(e){o=e;let t=100-(Date.now()-i);if(t<=0){a!==void 0&&(clearTimeout(a),a=void 0),s();return}a??=setTimeout(()=>{a=void 0,s()},t)},cancel(){a!==void 0&&(clearTimeout(a),a=void 0),o=void 0}}}function tt(e){return[...e.phases.map(e=>`[phase] ${H(e)}`),...e.narrations.map(e=>`[log] ${H(e)}`),...e.logs.map(e=>H(e))]}function nt(e){let t=e.error;if(t===void 0)return/* @__PURE__ */ Error(`internal error: a successful PTC run was rendered as a failure`);let n=tt(e),r=n.length===0?``:`\nCaptured output:\n${n.join(`
16
+ `)}`,i=Error(H(`code run failed (${t.kind}): ${t.message}${r}`));return i.name=`CodeRunFailedError`,i}function rt(e){let t=A(te(),`pi-ptc-output-${j()}.txt`);return E(t,e,`utf8`),t}function it(e,t){let n=e.totalLines-e.outputLines+1,r=e.totalLines;return e.lastLinePartial?`[Showing last ${_(e.outputBytes)} of line ${r}. Full output: ${t}]`:e.truncatedBy===`lines`?`[Showing lines ${n}-${r} of ${e.totalLines}. Full output: ${t}]`:`[Showing lines ${n}-${r} of ${e.totalLines} (${_(s)} limit). Full output: ${t}]`}function at(e){let{outcome:t,surface:n}=e,r=[...e.warnings??[]],i=t.images??[],a=[];t.phases.length>0&&a.push(`Phases: ${t.phases.map(e=>H(e)).join(` → `)}`),t.narrations.length>0&&a.push(t.narrations.map(e=>H(e)).join(`
17
+ `)),t.logs.length>0&&a.push(t.logs.map(e=>H(e)).join(`
18
+ `)),t.value!==void 0&&a.push(Ye(t.value));for(let e of r)a.push(`Warning: ${H(e)}`);let o=a.length>0?a.join(`
19
+ `):`(${Qe[n]} completed with no output)`,l=b(o,{maxLines:c,maxBytes:s}),u,d=l.content;return l.truncated&&(u=rt(o),d+=`\n\n${it(l,u)}`),{content:[{type:`text`,text:d},...i.map(e=>({type:`image`,data:e.data,mimeType:e.mimeType}))],details:{surface:n,logs:[...t.logs],narrations:[...t.narrations],phases:[...t.phases],warnings:r,...t.value===void 0?{}:{result:t.value},durationMs:e.durationMs,imageCount:i.length,...u===void 0?{}:{fullOutputPath:u},...t.subCalls===void 0?{}:{subCalls:t.subCalls}}}}const ot=/^(.*?)(└─ |├─ | )(?:PTC(?: workflow)?)([ ].*)$/;function st(e){return e.replace(/\x1b\[[0-9;]*m|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[\?[0-9;]*[a-zA-Z]/g,``)}function ct(e,t){if(e.charCodeAt(t)!==27)return t;let n=e.charCodeAt(t+1);if(n===93){let n=t+2;for(;n<e.length;){let t=e.charCodeAt(n);if(t===7)return n+1;if(t===27&&e.charCodeAt(n+1)===92)return n+2;n+=1}return n}if(n===91){let n=t+2;for(;n<e.length;){let t=e.charCodeAt(n);if(t>=64&&t<=126)return n+1;n+=1}return n}return t+2}function lt(e,t){let n=0,r=0;for(;r<t&&n<e.length;)e.charCodeAt(n)===27?n=ct(e,n):(n+=1,r+=1);return n}function ut(e,t){let n=t.intervalMs??150,{state:r}=t;t.isPartial?(r.startedAt??=Date.now(),r.interval??=setInterval(()=>t.requestInvalidate(),n)):(r.interval!==void 0&&(clearInterval(r.interval),r.interval=void 0),r.startedAt=void 0);let i=e.render.bind(e),a=e;return a.render=e=>{let a=i(e);return r.startedAt===void 0?a:dt(a,Math.floor((Date.now()-r.startedAt)/n),t.theme)},a.dispose=()=>{r.interval!==void 0&&(clearInterval(r.interval),r.interval=void 0)},e}function dt(e,t,n){return e.map(e=>{let r=st(e),i=ot.exec(r);if(i===null)return e;let a=(i[3]??``).slice(1);if(a.length===0)return e;let o=t%(a.length+1),s=i[0].length-a.length,c=lt(e,s),l=lt(e,s+a.length),u=o>=a.length?n.fg(`dim`,a):`${o>0?n.fg(`dim`,a.slice(0,o)):``}${n.fg(`accent`,a.slice(o,o+1))}${n.fg(`dim`,a.slice(o+1))}`;return`${e.slice(0,c)}${u}${e.slice(l)}`})}const ft={run_code:`PTC`,workflow:`PTC workflow`};var G=class{items;constructor(e){this.items=e}invalidate(){}render(e){let t=Math.max(1,Math.floor(e));return this.items.map(e=>pt(e,t))}};function pt(e,t){let{left:n,right:r}=e;if(r===void 0)return ae(n,t,`…`);let i=F(r);if(i>=t)return ae(r,t,`…`);let a=ae(n,Math.max(0,t-i-1),`…`),o=Math.max(1,t-i-F(a));return a+` `.repeat(o)+r}function K(e,t){return e.length>t?`${e.slice(0,t-1)}…`:e}function mt(e){return K((H(e).split(`
20
+ `).find(e=>e.trim().length>0)??``).trim(),120)}function ht(e){return e<1e3?`${e}ms`:`${(e/1e3).toFixed(2).replace(/\.?0+$/,``)}s`}function q(e,t){return`${e} ${t}${e===1?``:`s`}`}function gt(e){for(let t of e.split(`
21
+ `)){let e=t.trim();if(e.length!==0&&!e.startsWith(`//`)&&!e.startsWith(`/*`)&&!e.startsWith(`*`))return K(e.replace(/\s*\/\/.*$/,``),120)}}function _t(e){if(e===void 0)return;if(typeof e==`string`){let t=H(e).split(`
22
+ `),n=t.find(e=>e.trim().length>0)?.trim()??``,r=t.length>1?` (+${t.length-1} lines)`:``;return n.length===0&&r.length===0?`""`:K(n,60)+r}if(typeof e!=`object`||!e)return String(e);let t=Ye(e);if(!t.includes(`
23
+ `)&&t.length<=60)return t;if(Array.isArray(e))return`Array(${e.length})`;let n=Object.keys(e);return n.length===0?`{}`:`{${n.slice(0,4).join(`, `)}${n.length>4?`, +${n.length-4}`:``}}`}function vt(e,t){if(e===void 0)return;let n=[],r=e.logs.length+e.narrations.length;if(r>0&&n.push(t.fg(`dim`,q(r,`output line`))),e.imageCount>0&&n.push(t.fg(`toolOutput`,q(e.imageCount,`image`))),e.surface===`workflow`&&e.phases.length>0&&n.push(t.fg(`dim`,q(e.phases.length,`phase`))),e.warnings.length>0&&n.push(t.fg(`warning`,q(e.warnings.length,`warning`))),e.fullOutputPath!==void 0&&n.push(t.fg(`warning`,`truncated`)),e.durationMs>0&&n.push(t.fg(`dim`,ht(e.durationMs))),n.length!==0)return`${t.fg(`dim`,`•`)} ${n.join(t.fg(`dim`,` · `))}`}function yt(e){if(e!==void 0)return{surface:e.surface??`run_code`,logs:e.logs??[],narrations:e.narrations??[],phases:e.phases??[],warnings:e.warnings??[],durationMs:e.durationMs??0,imageCount:e.imageCount??0,...e.result===void 0?{}:{result:e.result},...e.fullOutputPath===void 0?{}:{fullOutputPath:e.fullOutputPath},...e.subCalls===void 0?{}:{subCalls:e.subCalls}}}function bt(e,t,n){let r=` `+(t?`└─ `:`├─ `),i=` `+(t?` `:`│ `),a=` `.repeat(8),o=[],s=e.lines[0]??``;o.push({left:`${r}${e.label.padEnd(8)}${n.fg(`dim`,s)}`});for(let t=1;t<e.lines.length;t++)o.push({left:`${i}${a}${n.fg(`dim`,e.lines[t]??``)}`});return e.more!==void 0&&e.more>0&&o.push({left:`${i}${a}…+${e.more} more lines`}),o}function xt(e){return typeof e!=`object`||!e?!1:Array.isArray(e)?e.length>0:Object.keys(e).length>0}const St=/^[A-Za-z_$][A-Za-z0-9_$]*$/;function Ct(e){return St.test(e)?e:JSON.stringify(e)}function J(e,t){if(e===null)return`null`;if(typeof e==`boolean`||typeof e==`number`)return String(e);if(typeof e==`string`){let n=H(e);if(n.length===0)return`""`;if(n.includes(`
24
+ `)){let e=K(n.replace(/\n/g,` `),Math.max(2,t-5));return JSON.stringify(e)+`…`}return n.length+2<=t?JSON.stringify(n):JSON.stringify(K(n,Math.max(2,t-5)))+`…`}return`?`}function wt(e){return typeof e!=`object`||!e}function Tt(e,t){if(t<8)return;if(Array.isArray(e)){if(e.length===0)return`[]`;if(e.length>5||!e.every(wt))return;let n=`[`+e.map(e=>J(e,Math.max(2,Math.floor(t/4)))).join(`, `)+`]`;return n.length<=t?n:void 0}if(typeof e!=`object`||!e)return;let n=Object.keys(e);if(n.length===0)return`{}`;if(n.length>5||!Object.values(e).every(wt))return;let r=`{`+n.map(n=>{let r=e[n]??null;return Ct(n)+`: `+J(r,Math.max(2,Math.floor(t/4)))}).join(`, `)+`}`;return r.length<=t?r:void 0}function Y(e,t,n,r,i,a,o,s,c){let l=n+(r?`└─ `:`├─ `),u=n+(r?` `:`│ `);if(s>=i){c.push(X(l+t+`…`,o));return}if(Array.isArray(e)&&e.length===0){c.push(X(l+t+`[]`,o));return}if(!Array.isArray(e)&&typeof e==`object`&&e&&Object.keys(e).length===0){c.push(X(l+t+`{}`,o));return}if(typeof e!=`object`||!e){let n=Math.max(0,o-F(l)-t.length);c.push(X(l+t+J(e,n),o));return}let d=Tt(e,Math.max(0,o-F(l)-t.length));if(d!==void 0){c.push(X(l+t+d,o));return}let f=Array.isArray(e),p=f?e.length:Object.keys(e).length,m;m=f?`Array(`+p+`)`:p>a?`{`+a+` of `+p+`}`:`{`+p+` keys}`,c.push(X(l+t+m,o));let h=Math.min(a,p);if(f)for(let t=0;t<h;t+=1){let n=t===h-1&&p<=h;Y(e[t]??null,`[`+t+`] `,u,n,i,a,o,s+1,c)}else{let t=Object.keys(e);for(let n=0;n<h;n+=1){let r=n===h-1&&p<=h,l=t[n];Y(e[l]??null,Ct(l)+`: `,u,r,i,a,o,s+1,c)}}if(p>h){let e=f?`items`:`keys`;c.push(X(u+`└─ …+`+(p-h)+` more `+e,o))}}function X(e,t){if(F(e)<=t)return e;let n=e.match(/^([\s│├└─]*)/),r=n&&n[1]?n[1]:``,i=F(r),a=Math.max(0,t-i-1),o=``,s=0;for(let t of e.slice(r.length)){let e=F(t);if(s+e>a)break;o+=t,s+=e}return r+o+`…`}function Et(e,t={}){let n=t.maxDepth??4,r=t.maxChildren??6,i=t.maxLineChars??120,a=t.moreAfter===!0;if(!xt(e))return typeof e==`object`&&e?[Array.isArray(e)?`[]`:`{}`]:[J(e,i)];let o=[];if(Array.isArray(e)){let t=e.length,s=Math.min(r,t);for(let c=0;c<s;c+=1){let l=!a&&c===s-1&&t<=s;Y(e[c]??null,`[`+c+`] `,``,l,n,r,i,0,o)}return t>s&&o.push((a?`├─ `:`└─ `)+`…+`+(t-s)+` more items`),o}let s=Object.keys(e),c=s.length,l=Math.min(r,c);for(let t=0;t<l;t+=1){let u=!a&&t===l-1&&c<=l,d=s[t];Y(e[d]??null,Ct(d)+`: `,``,u,n,r,i,0,o)}return c>l&&o.push((a?`├─ `:`└─ `)+`…+`+(c-l)+` more keys`),o}function Dt(e,t,n=`run_code`,r={}){let i=(typeof e.description==`string`&&e.description.trim().length>0?e.description.trim():e.meta?.name)??``,a=gt(e.code??e.script??``),o=i.length>0?i:a??`(no description)`;return ut(new G([{left:`└─ ${`${t.fg(`toolTitle`,t.bold(ft[n]))} ${t.fg(`accent`,o)}`}`}]),{intervalMs:150,isPartial:r.isPartial??!1,theme:t,requestInvalidate:r.requestInvalidate??Ot,state:r.state??{}})}function Ot(){}function kt(e){let t=e.content?.find(e=>e.type===`text`&&(e.text??``).length>0);return mt(t?.text??``)||`failed`}function At(e,t){if(e===`pi.dispatch`&&typeof t==`object`&&t&&`agent`in t)return`→ ${String(t.agent)}`;if(typeof t==`object`&&t){let n=t,r=e=>typeof n[e]==`string`?n[e]:void 0,i=e===`read`?r(`path`):e===`bash`?r(`command`):e===`grep`||e===`find`?r(`pattern`):e===`ls`||e===`edit`||e===`write`?r(`path`):void 0;if(typeof i==`string`)return i}try{let e=JSON.stringify(t).replace(/\n/g,` `);return e.length>40?`${e.slice(0,39)}…`:e}catch{return String(t).slice(0,40)}}const jt={running:{text:()=>`running`,themeSlot:`muted`},ok:{text:e=>e.durationMs===void 0?`ok`:`ok ${ht(e.durationMs)}`,themeSlot:`accent`},error:{text:e=>`failed: ${e.errorMessage??`error`}`,themeSlot:`error`},cancelled:{text:()=>`cancelled`,themeSlot:`muted`},rejected:{text:e=>`rejected: ${e.errorMessage??`concurrency`}`,themeSlot:`warning`}};function Mt(e,t){if(e.length===0)return[];let n=e.slice(0,32),r=e.length>32?e.length-32:0,i=[];return n.forEach((e,a)=>{let o=a===n.length-1&&r===0?`└─ `:`├─ `,s=e.name.length>=8?`${e.name} `:e.name.padEnd(8),c=At(e.name,e.args),l=jt[e.status];i.push({left:` ${o}${t.fg(`toolTitle`,s)}${t.fg(l.themeSlot,l.text(e))}${t.fg(`dim`,` `+c)}`})}),r>0&&i.push({left:` └─ …+${r} more calls`}),i}function Nt(e,t,n,r=!1,i=!1){let a=vt(e.details,n),o=e=>a===void 0?e:{...e,right:a};if(t)return[o({left:` ${n.fg(`error`,`failed: ${kt(e)}`)}`})];if(i&&e.details?.result===void 0)return[];let s=e.details?.result;if(s===void 0)return[o({left:` ${n.fg(`muted`,`done`)}`})];if(xt(s))return Et(s,{maxDepth:4,maxChildren:6,maxLineChars:120,moreAfter:r}).map((e,t)=>{let n={left:` ${e}`};return t===0&&a!==void 0?{...n,right:a}:n});let c=_t(s);return c===void 0?[o({left:` ${n.fg(`muted`,`done`)}`})]:[o({left:` ${n.fg(`dim`,`→ ${c}`)}`})]}function Pt(e,t,n,r={}){let i=yt(e.details),a=[...Nt({...e,details:i},t,n,!1,r.isPartial===!0)];return i?.subCalls!==void 0&&a.push(...Mt(i.subCalls,n)),new G(a)}function Ft(e,t,n,r,i={}){let a=yt(e.details);if(n){let t=[...Nt({...e,details:a},!0,r)];a?.subCalls!==void 0&&t.push(...Mt(a.subCalls,r));let n=H(e.content?.find(e=>e.type===`text`)?.text??kt(e));for(let e of n.split(`
25
+ `).slice(0,12))t.push({left:` ${r.fg(`error`,e)}`});return new G(t)}let o=[],s=t.code??t.script??``;if(s.length>0){let e=s.split(`
26
+ `).map(e=>K(e,120)),t=e.slice(0,3);o.push({label:`code`,lines:t,...e.length>t.length?{more:e.length-t.length}:{}})}if(a!==void 0&&a.surface===`workflow`&&a.phases.length>0){let e=a.phases.slice(0,8),t=a.phases.length>e.length?` …`:``;o.push({label:`phases`,lines:[`${e.join(` → `)}${t}`]})}if(a!==void 0&&a.narrations.length>0){let e=a.narrations.slice(0,12);o.push({label:`log`,lines:e.map(e=>H(e)),...a.narrations.length>e.length?{more:a.narrations.length-e.length}:{}})}if(a!==void 0&&a.logs.length>0){let e=a.logs.slice(0,12);o.push({label:`out`,lines:e.map(e=>H(e)),...a.logs.length>e.length?{more:a.logs.length-e.length}:{}})}if(a!==void 0&&a.warnings.length>0){let e=a.warnings.slice(0,4);o.push({label:`warn`,lines:e.map(e=>H(e)),...a.warnings.length>e.length?{more:a.warnings.length-e.length}:{}})}if(a!==void 0&&a.imageCount>0){let e=a.imageCount===1?`1 image attached`:`${a.imageCount} images attached`;o.push({label:`image`,lines:[e]})}let c=a?.subCalls,l=c!==void 0&&c.length>0||o.length>0,u=Nt({...e,details:a},!1,r,l,i.isPartial===!0);c!==void 0&&u.push(...Mt(c,r));for(let e=0;e<o.length;e+=1){let t=e===o.length-1;u.push(...bt(o[e]??{label:``,lines:[]},t,r))}return new G(u)}function It(e){return{renderCall(t,n,r){return Dt(t,n,e,{...r?.invalidate===void 0?{}:{requestInvalidate:r.invalidate},...r?.isPartial===void 0?{}:{isPartial:r.isPartial},...r?.state===void 0?{}:{state:r.state}})},renderResult(e,t,n,r){let i=t.isPartial===!0;return t.expanded===!0?Ft(e,r?.args??{},r?.isError===!0,n,{isPartial:i}):Pt(e,r?.isError===!0,n,{isPartial:i})}}}const Lt=["Run a TypeScript program that composes pi's tools in one shot. Required arguments: `code` —","the body of an async function (top-level `return` and `await` work; type annotations are","advisory, the code runs type-stripped) — and `description`, a 5-10 word summary of what the",`program does.`,``,"Inside the program, call this session's enabled built-in tools as `tools.<name>(args)` — e.g.",'`await tools.read({ path: "src/index.ts" })` or `await tools.bash({ command: "npm test" })`.',"The bound names mirror the session's active tools (a default session has `read`, `bash`, `edit`,","`write`); calling a name that is not bound rejects with an error the program can catch, and","independent calls may overlap under `Promise.all`.",``,"Only the program's return value and its `console.log` output come back. This surface has no","helpers: `log` / `phase` / `parallel` / `pipeline` exist only in `ptc_workflow`.",``,"Image-bearing tool results inside the program (a `tools.read` on a PNG, say) are attached to you",`after the run, so never return image data as the completion value — that only spends your context`,`on base64.`].join(`
27
+ `),Rt=x.Object({code:x.String({description:`The program: the body of an async TypeScript function.`}),description:x.String({description:`Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI).`}),timeoutMs:x.Optional(x.Number({description:`Elapsed deadline in milliseconds for the whole run, including tool calls. Omit it (or pass 0) for the default of ${R.timeoutMs/1e3} s; anything above ${R.maxTimeoutMs/1e3} s is capped.`}))});function zt(e={}){return g({name:`ptc_run_code`,label:`PTC Run Code`,description:Lt,promptSnippet:`Run a TypeScript program that composes pi's tools in one shot (tools.<name>(args)); only its return value and console.log come back`,promptGuidelines:[...Ze],parameters:Rt,async execute(t,n,r,i,a){let o=$e(a),s=W(e.getBindingSourceNames?.()),c=Date.now(),l=e.getPool?.(),u=et({surface:`run_code`,startedAt:c,onUpdate:i}),d;try{d=await Re({code:n.code,surface:`run_code`,cwd:o,bindings:Se({cwd:o,names:s,includeDispatch:!0}),...e.depth===void 0?{}:{depth:e.depth},...n.timeoutMs===void 0?{}:{timeoutMs:n.timeoutMs},...r===void 0?{}:{signal:r},...e.config===void 0?{}:{config:e.config},...l===void 0?{}:{pool:l},onSubCallChange:e=>u.update(e)})}finally{u.cancel()}if(d.error!==void 0)throw nt(d);return at({outcome:d,surface:`run_code`,durationMs:Date.now()-c})},...It(`run_code`)})}const Bt=[`Run a structured TypeScript workflow: a named plan that reports phases and narration as it`,"goes. `script` is the body of an async function, `meta` names it (`{ name, description, phases? }`,","where `phases` is the ordered plan), and optional `args` is plain-JSON input bound to the","program's `args` global.",``,"Helpers inside the script: `log(message)` narrates progress; `phase(title)` frames the workflow","(titles outside `meta.phases` are reported as warnings); `parallel(thunks)` runs thunks","concurrently, a failed item becoming `null`; `pipeline(items, ...stages)` chains stages per item","with the same per-item `null` on failure. There is no `agent()` helper.",``,"Tools are reachable as `tools.<name>(args)` exactly as in `ptc_run_code`, and the bound names",`mirror the session's enabled tools. What comes back is the script's return value, its`,"`log`/`phase` narration and its `console.log` output.",``,`Image-bearing tool results inside the script are attached to you after the run — never return`,`image data as the script's value.`].join(`
28
+ `),Vt=x.Object({meta:x.Object({name:x.String({description:`Short workflow name, shown in the UI.`}),description:x.String({description:`What this workflow does, in active voice.`}),phases:x.Optional(x.Array(x.Object({name:x.String({description:`Phase title, matched against phase(title) calls.`})}),{description:`Ordered plan of the workflow's phases. phase() titles outside this list are reported as warnings.`}))},{description:`Workflow metadata: what this run is and which phases it declares.`}),script:x.String({description:`The program: the body of an async TypeScript function.`}),args:x.Optional(x.Record(x.String(),x.Unknown(),{description:"Plain-JSON input bound to the program's `args` global. No functions, symbols, undefined values or cycles."}))});function Ht(t){if(t!==void 0){if(typeof t!=`object`||!t||Array.isArray(t))throw TypeError(`ptc_workflow args must be a plain JSON object, received ${e(t)}`);for(let e of Object.keys(t)){let n=Ut(t[e],`args.${e}`,/* @__PURE__ */ new Set);if(n!==void 0)throw TypeError(`ptc_workflow args must be plain JSON: ${n}`)}}}function Ut(t,n,r){if(t===null||typeof t==`string`||typeof t==`boolean`)return;if(t===void 0)return`${n} is undefined`;if(typeof t==`number`)return Number.isFinite(t)?void 0:`${n} is ${String(t)}, which is not representable as JSON`;if(typeof t!=`object`)return`${n} is ${e(t)}`;let i=t;if(r.has(i))return`${n} is a circular reference`;r.add(i);try{if(Array.isArray(i)){for(let e=0;e<i.length;e+=1){let t=Ut(i[e],`${n}[${e}]`,r);if(t!==void 0)return t}return}let e=Object.getPrototypeOf(i);if(e!==Object.prototype&&e!==null){let e=i.constructor?.name;return`${n} is a ${typeof e==`string`&&e.length>0?e:`non-plain object`}, not plain JSON`}for(let e of Object.keys(i)){let t=Ut(i[e],`${n}.${e}`,r);if(t!==void 0)return t}return}finally{r.delete(i)}}function Wt(e,t){if(t===void 0||t.length===0)return[];let n=new Set(t),r=/* @__PURE__ */ new Set,i=[];for(let t of e.phases)n.has(t)||r.has(t)||(r.add(t),i.push(`phase "${t}" is not listed in meta.phases (declared: ${[...n].join(`, `)})`));return i}function Gt(e={}){return g({name:`ptc_workflow`,label:`PTC Workflow`,description:Bt,promptSnippet:`Run a structured TypeScript workflow with named phases, narration (log/phase) and structured concurrency (parallel/pipeline)`,promptGuidelines:[...Ze],parameters:Vt,async execute(t,n,r,i,a){Ht(n.args);let o=$e(a),s=W(e.getBindingSourceNames?.()),c=Date.now(),l=e.getPool?.(),u=et({surface:`workflow`,startedAt:c,onUpdate:i}),d;try{d=await Re({code:n.script,surface:`workflow`,cwd:o,bindings:Se({cwd:o,names:s,includeDispatch:!0}),...n.args===void 0?{}:{args:n.args},...e.depth===void 0?{}:{depth:e.depth},...r===void 0?{}:{signal:r},...e.config===void 0?{}:{config:e.config},...l===void 0?{}:{pool:l},onSubCallChange:e=>u.update(e)})}finally{u.cancel()}if(d.error!==void 0)throw nt(d);let f=n.meta.phases?.map(e=>e.name);return at({outcome:d,surface:`workflow`,warnings:Wt(d,f),durationMs:Date.now()-c})},...It(`workflow`)})}var Kt=class{#e;#t=/* @__PURE__ */ new Map;constructor(e={}){this.#e=Te(e.config)}get(e){let t=this.#t.get(e);if(t!==void 0)return t;let n=we(),r=new ke({buildWorkerUrl:Me,size:this.#e.poolSize,acquireTimeoutMs:this.#e.poolAcquireTimeoutMs,drainGraceMs:this.#e.drainGraceMs,workerOptions:Oe({surface:e,env:n,limits:this.#e})});return this.#t.set(e,r),r}async drain(){let e=[...this.#t.values()];this.#t.clear(),await Promise.all(e.map(e=>e.drain()))}};const Z=[`ptc_run_code`,`ptc_workflow`],qt=[`read`,`bash`,`edit`,`write`],Q=`ptc-mode`,Jt=`ptc-mode`,Yt=`ptc.json`,Xt=`builtins-only`;function Zt(){return{enabled:!1,base:void 0,ourLoadout:void 0}}function Qt(e){let t=A(e,Yt),n;try{n=T(t,`utf8`)}catch{return{defaultMode:!0,source:`default`}}let r;try{r=JSON.parse(n)}catch(e){return{defaultMode:!0,source:`invalid`,error:`${t} is not valid JSON (${e instanceof Error?e.message:String(e)})`}}if(typeof r!=`object`||!r||Array.isArray(r))return{defaultMode:!0,source:`invalid`,error:`${t} must contain a JSON object`};let i=r.defaultMode;return i===void 0?{defaultMode:!0,source:`default`}:typeof i==`boolean`?{defaultMode:i,source:`file`}:{defaultMode:!0,source:`invalid`,error:`${t}: "defaultMode" must be a boolean, received ${typeof i}`}}function $t(e){return[...new Set(e)]}function en(e,t){if(e.length!==t.length)return!1;let n=[...e].sort(),r=[...t].sort();return n.every((e,t)=>e===r[t])}function tn(e,t){let n=e=>Z.includes(e);if(t===`all-but-ptc`)return $t(e.filter(n));let r=new Set(L);return $t(e.filter(e=>n(e)||!r.has(e)))}function nn(e){let{mode:t,defaultMode:n,active:r,manual:i}=e;return t===`tui`?!n&&!i?{enter:!1,reason:`config-off`}:Z.some(e=>r.includes(e))?!i&&qt.filter(e=>!r.includes(e)).length>0?{enter:!1,reason:`restricted-session`}:{enter:!0,base:$t(r),loadout:tn(r,e.hide??`builtins-only`)}:{enter:!1,reason:`tools-unavailable`}:{enter:!1,reason:`not-tui`}}function rn(e,t){return e.enabled&&e.base!==void 0?e.base:t}function an(e,t){return!e.enabled||e.ourLoadout===void 0?!1:!en(e.ourLoadout,t)}function on(e,t){let n=t.length>0&&t.every(e=>Z.includes(e));return e?.enabled===!0&&n&&e.base!==void 0?{base:e.base,restoreFirst:!0}:{base:t,restoreFirst:!1}}function sn(e,t){let n=t===`all-but-ptc`?`Every built-in tool is hidden from you, and so are other extensions' tools.`:`The built-in tools (read, bash, edit, write, grep, find, ls) are hidden from you.`,r=e.map(e=>`tools.${e}(args)`),i=e.slice(0,2).map(e=>e===`read`?`await tools.read({ path: "src/index.ts" })`:e===`bash`?`await tools.bash({ command: "pnpm test" })`:`await tools.${e}({ /* … */ })`).join(`
29
+ `);return[`[PTC MODE ACTIVE]`,`This session runs in PTC mode. `+n,``,`Work by writing one TypeScript program per task and running it:`,` - ptc_run_code for a plain program (its return value and console.log come back to you)`,` - ptc_workflow when the task has named phases and you want narration (log / phase /`,` parallel / pipeline are available there)`,``,`The program body is an async function. Available inside it: ${r.join(`, `)}.`,i.length>0?`Examples:\n${i}`:``,``,`Batch a task's steps into ONE program instead of issuing many tool calls — gathering`,`intermediate output yourself and returning only what matters is the point of this mode.`,`Image-bearing tool results inside a program are attached to you after the run — never return`,`base64 image data as your program's value.`,`If the PTC surfaces cannot express what you need, say so and the user can run /ptc off.`].filter(e=>e.length>0).join(`
30
+ `)}const cn=[`read`,`bash`],ln=`Use the read tool to load a skill's file when the task matches its description.`,un=`Use tools.read({ path }) inside a ptc_run_code program to load a skill's file when the task matches its description.`;function dn(e){return!cn.some(t=>e.includes(t))}function fn(e,t=v){let n=t([...e],`read`).trim();return n.length===0?``:n.includes(ln)?n.replace(ln,un):`${un}\n\n${n}`}const $=Xt;function pn(e,t){return[`PTC mode on — ${t===`all-but-ptc`?`all other tools`:`built-in tools (now reachable only as tools.<name> inside a program)`} hidden.`,`Bindings available inside a program: ${W(e).join(`, `)||`(none)`}.`,`Run /ptc off to restore the normal tool set.`].join(`
31
+ `)}function mn(e){let t=Zt(),n=!1,r=new Kt,i=()=>rn(t,e.getActiveTools()),a=Xe();e.registerTool(zt({getBindingSourceNames:i,getPool:()=>r.get(`run_code`),depth:a})),e.registerTool(Gt({getBindingSourceNames:i,getPool:()=>r.get(`workflow`),depth:a})),e.on(`turn_end`,async()=>{let e=r;r=new Kt,await e.drain()});let o=e=>{e.ui.setStatus(Jt,t.enabled?e.ui.theme.fg(`accent`,`PTC`):void 0)},s=(r,i,a,s)=>{t.enabled=!0,t.base=r,t.ourLoadout=i,e.setActiveTools([...i]),n=!0,e.appendEntry(Q,{enabled:!0,base:[...r]}),o(a),a.ui.notify(s,`info`)},c=(r,i)=>{let a=t.base;t.enabled=!1,t.base=void 0,t.ourLoadout=void 0,n=!1,a!==void 0&&e.setActiveTools([...a]),e.appendEntry(i.customType,{enabled:!1}),o(r),i.announce&&r.ui.notify(`PTC mode off — built-in tools are directly callable again.`,`info`)},l=e=>{let t=e.sessionManager.getEntries();for(let e=t.length-1;e>=0;--e){let n=t[e];if(n?.type!==`custom`||n.customType!==`ptc-mode`)continue;let r=n.data,i=Array.isArray(r?.base)?r.base.filter(e=>typeof e==`string`):void 0;return{enabled:r?.enabled===!0,...i===void 0?{}:{base:i}}}};e.registerCommand(`ptc`,{description:`Show or toggle PTC mode (built-in tools reachable only from inside a PTC program)`,handler:async(n,r)=>{let i=n.trim().toLowerCase();if(i===`off`){if(!t.enabled){r.ui.notify(`PTC mode is already off.`,`info`);return}c(r,{announce:!0,customType:Q});return}if(i!==`on`){r.ui.notify(t.enabled?`PTC mode is ON — run /ptc off to restore built-in tools.\nBindings: ${W(t.base).join(`, `)||`(none)`}`:`PTC mode is OFF — run /ptc on to enter it.`,`info`);return}if(t.enabled){r.ui.notify(`PTC mode is already on.`,`info`);return}let a=nn({mode:r.mode,defaultMode:!0,active:e.getActiveTools(),manual:!0,hide:$});if(!a.enter){r.ui.notify(a.reason===`tools-unavailable`?`Cannot enter PTC mode: ptc_run_code / ptc_workflow are not active in this session.`:`Cannot enter PTC mode in this run mode.`,`warning`);return}s(a.base,a.loadout,r,pn(a.base,$))}}),e.on(`session_start`,async(t,n)=>{let r=Qt(y());r.error!==void 0&&n.ui.notify(`pi-ptc-subagents: ${r.error}`,`warning`);let i=l(n),{base:a,restoreFirst:c}=on(i,e.getActiveTools());c&&e.setActiveTools([...a]);let u=nn({mode:n.mode,defaultMode:r.defaultMode,active:a,manual:!1,hide:$}),d=i?.enabled??u.enter;if(u.enter&&d){s(a,u.loadout,n,pn(a,$));return}o(n),!u.enter&&u.reason!==`not-tui`&&i===void 0&&u.reason===`restricted-session`&&n.ui.notify(`PTC mode skipped: this session was launched with an explicit tool restriction.`,`info`)});let u=!1,d=(e,n)=>{if(e===void 0)return;if(!t.enabled||!n){u&&=(delete e.sections.skills,!1);return}let r=fn(e.skills);r.length!==0&&(e.sections.skills=r,u=!0)};e.on(`before_agent_start`,async(r,i)=>{let a=r.systemPromptOptions;if(d(a,dn(e.getActiveTools())),t.enabled){if(an(t,e.getActiveTools())){c(i,{announce:!1,customType:Q}),d(a,dn(e.getActiveTools())),i.ui.notify(`PTC mode off — another extension changed the active tool set.`,`warning`);return}if(n)return n=!1,{message:{customType:`ptc-mode-briefing`,content:sn(W(t.base),$),display:!1}}}})}export{L as BUILTIN_BINDING_NAMES,be as DEFAULT_BINDING_NAMES,R as DEFAULT_CONFIG,Xt as DEFAULT_HIDE_STRATEGY,o as HOST_FRAME_KIND,ze as MAX_LINE_CHARS,qt as MODE_REQUIRED_TOOL_NAMES,n as PTC_ERROR_KIND,i as PTC_LOG_LEVEL,Yt as PTC_MODE_CONFIG_FILE,Q as PTC_MODE_ENTRY_TYPE,Jt as PTC_MODE_STATUS_KEY,Z as PTC_MODE_TOOL_NAMES,un as PTC_SKILL_LOAD_INSTRUCTION,cn as SKILL_READING_TOOL_NAMES,Kt as TurnPools,Ce as WORKER_ENV_ALLOW_LIST,t as WORKER_FRAME_KIND,ke as WorkerPool,rn as bindingSource,sn as buildModeInstruction,fn as buildPtcSkillsSection,Se as createBuiltinBindings,we as createWorkerEnv,nn as decideModeEntry,mn as default,an as detectExternalLoadoutChange,Ee as effectiveTimeoutMs,Zt as initialModeState,tn as modeLoadout,Qt as readDefaultModeConfig,Ye as renderModelValue,on as resolveBaseOnStart,Te as resolveConfig,Re as runPtcProgram,en as sameToolSet,H as sanitizeText,dn as skillsSectionDropped,Ve as stripAnsi};
3130
32
  //# sourceMappingURL=index.js.map