pi-ptc-subagents 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,35 @@ URL tracks the latest GitHub Release (pinned URLs get no update checks); run `/r
81
81
  activate. A bundled `ptc` skill teaches the model when to write a program instead of
82
82
  fanning out tool calls one by one.
83
83
 
84
+ ## The engine on its own (no model, no pi)
85
+
86
+ The PTC runtime is a zero-dependency engine inside this package. Install it and run programs
87
+ with no model in the loop — a batch job, a CI step, or a script you wrote yourself.
88
+
89
+ ```bash
90
+ npm i pi-ptc-subagents
91
+ ```
92
+
93
+ ```ts
94
+ import { createFsBindings, runPtcProgram } from "pi-ptc-subagents/ptc";
95
+
96
+ const outcome = await runPtcProgram({
97
+ code: `const files = await tools.glob({ pattern: "*.md" });
98
+ return { files, hits: await tools.grep({ pattern: "TODO", path: "." }) };`,
99
+ bindings: createFsBindings(process.cwd()),
100
+ cwd: process.cwd(),
101
+ });
102
+ ```
103
+
104
+ Or from the shell:
105
+
106
+ ```bash
107
+ ptc prog.ts --workdir .
108
+ ```
109
+
110
+ The program sees the same vm cell as on pi: `tools.*` is the only door out, and the file tool
111
+ set is confined to `workdir`. The engine pulls in nothing — no pi, no dependencies at all.
112
+
84
113
  ## Install (pi)
85
114
 
86
115
  ```bash
@@ -128,9 +157,14 @@ full pi process running as you. `bash` is not in the program binding table on ei
128
157
 
129
158
  pi `>= 0.86.0`, tracked against latest (currently 1.4.0). Two version facts matter:
130
159
 
131
- - **In-program `tools.*` needs pi ≥ 0.99.** On 0.86/0.87 a program still evaluates and can
132
- still dispatch a task, but session-tool calls fail with a clear version error. The
133
- codemode surface itself also only exists from 0.99.
160
+ - **In-program session tools need pi ≥ 0.99 for the nested call channel.** On 0.86/0.87 a
161
+ program still evaluates, still dispatches tasks, and still calls `read` / `write` / `edit` /
162
+ `grep` / `ls` / `find` — pi has no nested tool-call channel there, so the extension builds
163
+ pi's own tool definitions (its public `create*ToolDefinition` factories) and calls them
164
+ directly. Same tools, same parameter shapes as 0.99; only the call path differs. The
165
+ codemode surface itself still only exists from 0.99. That fallback is gated on the pi
166
+ version: on 0.99+ a missing call channel is an error, not a silent bypass of pi's tool
167
+ pipeline.
134
168
  - Everything else in the 0.86–1.4 range is covered by real-machine smoke tests, including
135
169
  a program that dispatches a real child process. Per-version details:
136
170
  [docs/pi-version-matrix.md](./docs/pi-version-matrix.md).
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as piHost from "@earendil-works/pi-coding-agent";
1
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
4
  import { join } from "node:path";
@@ -3905,34 +3906,73 @@ var TaskEngine = class {
3905
3906
  }
3906
3907
  };
3907
3908
  // ../tui/src/bounds.ts
3908
- const RENDER_BOUNDS$1 = {
3909
+ const RENDER_BOUNDS = {
3909
3910
  MAX_LOG_LINES_EXPANDED: 12,
3910
3911
  VALUE_TREE_MAX_DEPTH: 4,
3911
3912
  VALUE_TREE_MAX_CHILDREN: 6,
3912
3913
  VALUE_TREE_MAX_LINE_CHARS: 120
3913
3914
  };
3914
3915
  // ../tui/src/text.ts
3915
- const ANSI_PATTERN$1 = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
3916
- function stripAnsi$1(text) {
3917
- return text.replace(ANSI_PATTERN$1, "");
3916
+ const ANSI_PATTERN = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
3917
+ function stripAnsi(text) {
3918
+ return text.replace(ANSI_PATTERN, "");
3918
3919
  }
3919
- function sanitizeText$1(text) {
3920
- return stripAnsi$1(text.replace(/\r/g, ""));
3920
+ function sanitizeText(text) {
3921
+ return stripAnsi(text.replace(/\r/g, ""));
3921
3922
  }
3922
- const MAX_LINE_CHARS$1 = RENDER_BOUNDS$1.VALUE_TREE_MAX_LINE_CHARS;
3923
- function truncateLine$1(line, maxChars = MAX_LINE_CHARS$1) {
3923
+ const MAX_LINE_CHARS = RENDER_BOUNDS.VALUE_TREE_MAX_LINE_CHARS;
3924
+ function truncateLine(line, maxChars = MAX_LINE_CHARS) {
3924
3925
  if (line.length <= maxChars) return line;
3925
3926
  let cut = maxChars;
3926
3927
  while (cut > 0 && (line.charCodeAt(cut) & 64512) === 56320) cut--;
3927
3928
  return `${line.slice(0, cut)}…`;
3928
3929
  }
3930
+ function renderModelValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
3931
+ const pad = " ".repeat(depth);
3932
+ if (depth >= RENDER_BOUNDS.VALUE_TREE_MAX_DEPTH) return "…";
3933
+ if (typeof value === "string") return depth === 0 ? value : JSON.stringify(value);
3934
+ if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
3935
+ if (undefinedValue(value)) return "(无完成值)";
3936
+ if (value !== null && typeof value === "object") {
3937
+ if (seen.has(value)) return "[Circular]";
3938
+ seen.add(value);
3939
+ }
3940
+ if (Array.isArray(value)) {
3941
+ const shown = value.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
3942
+ const rest = value.length - shown.length;
3943
+ const oneLine = `[${shown.map((v) => JSON.stringify(v)).join(", ")}]`;
3944
+ const rendered = value.length === 0 ? "[]" : rest === 0 && shown.every((v) => primitive(v)) && oneLine.length <= MAX_LINE_CHARS ? oneLine : `[\n${shown.map((v) => truncateLine(`${pad} ${renderModelValue(v, depth + 1, seen)}`)).join(",\n")}${rest > 0 ? `,\n${pad} …(余 ${rest} 项)` : ""}\n${pad}]`;
3945
+ seen.delete(value);
3946
+ return rendered;
3947
+ }
3948
+ if (value !== null && typeof value === "object") {
3949
+ const entries = Object.entries(value);
3950
+ const shown = entries.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
3951
+ const rest = entries.length - shown.length;
3952
+ const oneLine = `{ ${shown.map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
3953
+ const rendered = entries.length === 0 ? "{}" : rest === 0 && shown.every(([, v]) => primitive(v)) && oneLine.length <= MAX_LINE_CHARS ? oneLine : `{\n${shown.map(([k, v]) => truncateLine(`${pad} ${k}: ${renderModelValue(v, depth + 1, seen)}`)).join(",\n")}${rest > 0 ? `,\n${pad} …(余 ${rest} 项)` : ""}\n${pad}}`;
3954
+ seen.delete(value);
3955
+ return rendered;
3956
+ }
3957
+ return String(value);
3958
+ }
3959
+ function primitive(v) {
3960
+ return v === null || [
3961
+ "string",
3962
+ "number",
3963
+ "boolean"
3964
+ ].includes(typeof v);
3965
+ }
3966
+ function undefinedValue(value) {
3967
+ return typeof value === "object" && value !== null && value.$void === true;
3968
+ }
3929
3969
  // ../tui/src/truncate.ts
3930
- const TRUNCATION_MAX_BYTES$1 = 51200;
3931
- const TMP_FILE_TTL_MS$1 = 864e5;
3932
- const TMP_FILE_PREFIX$1 = "pi-ptc-output-";
3933
- let sweptTmp$1 = false;
3934
- function sweepExpiredTmp$1() {
3935
- sweptTmp$1 = true;
3970
+ const TRUNCATION_MAX_BYTES = 51200;
3971
+ const TMP_FILE_TTL_MS = 864e5;
3972
+ const TMP_FILE_PREFIX = "pi-ptc-output-";
3973
+ let sweptTmp = false;
3974
+ function sweepExpiredTmp() {
3975
+ sweptTmp = true;
3936
3976
  let names;
3937
3977
  try {
3938
3978
  names = readdirSync(tmpdir());
@@ -3940,14 +3980,14 @@ function sweepExpiredTmp$1() {
3940
3980
  return;
3941
3981
  }
3942
3982
  for (const name of names) {
3943
- if (!name.startsWith(TMP_FILE_PREFIX$1)) continue;
3983
+ if (!name.startsWith(TMP_FILE_PREFIX)) continue;
3944
3984
  try {
3945
3985
  const file = join(tmpdir(), name);
3946
- if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS$1) rmSync(file, { force: true });
3986
+ if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS) rmSync(file, { force: true });
3947
3987
  } catch {}
3948
3988
  }
3949
3989
  }
3950
- function applyAdr0015Truncation$1(text) {
3990
+ function applyAdr0015Truncation(text) {
3951
3991
  const lines = text.split("\n");
3952
3992
  const bytes = Buffer.byteLength(text, "utf8");
3953
3993
  if (bytes <= 51200 && lines.length <= 2e3) return {
@@ -3961,11 +4001,11 @@ function applyAdr0015Truncation$1(text) {
3961
4001
  kept = keptLines.join("\n");
3962
4002
  }
3963
4003
  const keptBuffer = Buffer.from(kept, "utf8");
3964
- if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES$1).toString("utf8");
3965
- const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX$1}${randomUUID()}.txt`);
4004
+ if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES).toString("utf8");
4005
+ const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX}${randomUUID()}.txt`);
3966
4006
  mkdirSync(tmpdir(), { recursive: true });
3967
4007
  writeFileSync(fullOutputPath, text, "utf8");
3968
- if (!sweptTmp$1) sweepExpiredTmp$1();
4008
+ if (!sweptTmp) sweepExpiredTmp();
3969
4009
  return {
3970
4010
  text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
3971
4011
  truncated: true,
@@ -3988,10 +4028,10 @@ function projectTaskEntry(record, options = {}) {
3988
4028
  return {
3989
4029
  id: record.id,
3990
4030
  status: record.status,
3991
- ...record.label ? { label: truncateLine$1(record.label, LABEL_MAX_CHARS) } : {},
4031
+ ...record.label ? { label: truncateLine(record.label, LABEL_MAX_CHARS) } : {},
3992
4032
  ...record.finishedAt !== void 0 ? { finishedAt: record.finishedAt } : {},
3993
- ...record.task ? { task: truncateLine$1(record.task, TASK_TEXT_MAX_CHARS) } : {},
3994
- ...record.report?.summary ? { summary: truncateLine$1(record.report.summary, SUMMARY_MAX_CHARS) } : {},
4033
+ ...record.task ? { task: truncateLine(record.task, TASK_TEXT_MAX_CHARS) } : {},
4034
+ ...record.report?.summary ? { summary: truncateLine(record.report.summary, SUMMARY_MAX_CHARS) } : {},
3995
4035
  ...options.archived ? { archived: true } : {},
3996
4036
  ...options.withSession && record.sessionId ? { sessionId: record.sessionId } : {}
3997
4037
  };
@@ -4250,9 +4290,9 @@ async function readTaskOutput(source, record) {
4250
4290
  const tail = await source.readOutputTail(taskId, 51200) ?? "";
4251
4291
  let answer = parseChildStream(tail).text;
4252
4292
  if (!answer && outputBytes > 51200) answer = parseChildStream(await source.readOutput(taskId) ?? "").text;
4253
- if (answer) return { text: applyAdr0015Truncation$1(answer).text };
4293
+ if (answer) return { text: applyAdr0015Truncation(answer).text };
4254
4294
  return {
4255
- output: applyAdr0015Truncation$1(tail).text,
4295
+ output: applyAdr0015Truncation(tail).text,
4256
4296
  guidance: renderLongOutputGuidance({
4257
4297
  bytes: outputBytes,
4258
4298
  report: record.report,
@@ -4512,14 +4552,14 @@ async function runWebSearch(action, context) {
4512
4552
  };
4513
4553
  return {
4514
4554
  ok: true,
4515
- text: sanitizeText$1(renderSearchOverview(outcome.value, budget))
4555
+ text: sanitizeText(renderSearchOverview(outcome.value, budget))
4516
4556
  };
4517
4557
  }
4518
4558
  if (action.kind === "open") {
4519
4559
  const cached = context.memory.lookup(action.url);
4520
4560
  if (cached !== void 0) return {
4521
4561
  ok: true,
4522
- text: sanitizeText$1(`(记忆命中,未发请求)\n${cached.slice(0, budget)}`)
4562
+ text: sanitizeText(`(记忆命中,未发请求)\n${cached.slice(0, budget)}`)
4523
4563
  };
4524
4564
  if (!context.quota.tryAcquire()) return {
4525
4565
  ok: false,
@@ -4538,7 +4578,7 @@ async function runWebSearch(action, context) {
4538
4578
  context.memory.storeText(action.url, outcome.value);
4539
4579
  return {
4540
4580
  ok: true,
4541
- text: sanitizeText$1(outcome.value.slice(0, budget))
4581
+ text: sanitizeText(outcome.value.slice(0, budget))
4542
4582
  };
4543
4583
  }
4544
4584
  let regex;
@@ -4558,7 +4598,7 @@ async function runWebSearch(action, context) {
4558
4598
  })).filter(({ line }) => regex.test(line));
4559
4599
  return {
4560
4600
  ok: true,
4561
- text: sanitizeText$1(lines.length > 0 ? lines.slice(0, 50).map(({ line, index }) => `${index + 1}: ${line}`).join("\n") : "(页内无匹配行)").slice(0, budget)
4601
+ text: sanitizeText(lines.length > 0 ? lines.slice(0, 50).map(({ line, index }) => `${index + 1}: ${line}`).join("\n") : "(页内无匹配行)").slice(0, budget)
4562
4602
  };
4563
4603
  }
4564
4604
  /** 页文获取:记忆命中免费;否则占额抓取并回存记忆 */
@@ -4651,50 +4691,6 @@ function createWebDsSearchTool(context) {
4651
4691
  }
4652
4692
  };
4653
4693
  }
4654
- // ../ptc/src/task-binding.ts
4655
- const TASK_BINDING_NAME = "pi_task_ptc";
4656
- const TASK_DOOR_PARAMETERS = _Object_({
4657
- task: String$1({
4658
- minLength: 1,
4659
- description: TASK_DOOR_PARAM_DESCRIPTIONS.task
4660
- }),
4661
- agent: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.agent })),
4662
- why: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.why })),
4663
- agentScope: Optional(Union([
4664
- Literal("user"),
4665
- Literal("project"),
4666
- Literal("both")
4667
- ], { description: TASK_DOOR_PARAM_DESCRIPTIONS.agentScope })),
4668
- model: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.model })),
4669
- thinkingLevel: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.thinkingLevel })),
4670
- wait: Optional(Boolean$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.wait })),
4671
- label: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.label })),
4672
- readOnly: Optional(Union([Literal("strict"), Literal("lenient")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.readOnly })),
4673
- timeoutMs: Optional(Number$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.timeoutMs })),
4674
- projectContext: Optional(Union([Literal("auto"), Literal("none")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.projectContext }))
4675
- });
4676
- function createTaskBinding(door) {
4677
- return {
4678
- name: TASK_BINDING_NAME,
4679
- parameters: TASK_DOOR_PARAMETERS,
4680
- execute(args) {
4681
- const input = args ?? {};
4682
- return door.createTask(input.task, {
4683
- label: input.label,
4684
- wait: input.wait,
4685
- why: input.why,
4686
- model: input.model,
4687
- thinkingLevel: input.thinkingLevel,
4688
- agent: input.agent,
4689
- agentScope: input.agentScope,
4690
- readOnly: input.readOnly,
4691
- timeoutMs: input.timeoutMs,
4692
- projectContext: input.projectContext
4693
- });
4694
- }
4695
- };
4696
- }
4697
- const TASK_RECORDS_BINDING_NAME = "task_records";
4698
4694
  // ../ptc/src/limits.ts
4699
4695
  const DEFAULT_LIMITS = {
4700
4696
  timeoutMs: 12e4,
@@ -5023,127 +5019,7 @@ var WorkerPool = class {
5023
5019
  this.#spawned = Math.max(0, this.#spawned - 1);
5024
5020
  }
5025
5021
  };
5026
- // ../ptc/src/bindings.ts
5027
- const KNOWN_BINDING_NAMES = [
5028
- "read",
5029
- "bash",
5030
- "grep",
5031
- "find",
5032
- "ls",
5033
- "edit",
5034
- "write"
5035
- ];
5036
- const OPT_IN_BINDING_NAMES = ["bash"];
5037
- const DEFAULT_BINDING_NAMES = KNOWN_BINDING_NAMES.filter((name) => !OPT_IN_BINDING_NAMES.includes(name));
5038
- function resolveBindingNames(activeToolNames, optIns = []) {
5039
- const allowed = /* @__PURE__ */ new Set([...DEFAULT_BINDING_NAMES, ...optIns]);
5040
- return KNOWN_BINDING_NAMES.filter((name) => activeToolNames.includes(name) && allowed.has(name));
5041
- }
5042
- // ../output/dist/index.js
5043
- const RENDER_BOUNDS = {
5044
- MAX_LOG_LINES_EXPANDED: 12,
5045
- VALUE_TREE_MAX_DEPTH: 4,
5046
- VALUE_TREE_MAX_CHILDREN: 6,
5047
- VALUE_TREE_MAX_LINE_CHARS: 120
5048
- };
5049
- const ANSI_PATTERN = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
5050
- function stripAnsi(text) {
5051
- return text.replace(ANSI_PATTERN, "");
5052
- }
5053
- function sanitizeText(text) {
5054
- return stripAnsi(text.replace(/\r/g, ""));
5055
- }
5056
- const MAX_LINE_CHARS = RENDER_BOUNDS.VALUE_TREE_MAX_LINE_CHARS;
5057
- function truncateLine(line, maxChars = MAX_LINE_CHARS) {
5058
- if (line.length <= maxChars) return line;
5059
- let cut = maxChars;
5060
- while (cut > 0 && (line.charCodeAt(cut) & 64512) === 56320) cut--;
5061
- return `${line.slice(0, cut)}…`;
5062
- }
5063
- function renderModelValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
5064
- const pad = " ".repeat(depth);
5065
- if (depth >= RENDER_BOUNDS.VALUE_TREE_MAX_DEPTH) return "…";
5066
- if (typeof value === "string") return depth === 0 ? value : JSON.stringify(value);
5067
- if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
5068
- if (undefinedValue(value)) return "(无完成值)";
5069
- if (value !== null && typeof value === "object") {
5070
- if (seen.has(value)) return "[Circular]";
5071
- seen.add(value);
5072
- }
5073
- if (Array.isArray(value)) {
5074
- const shown = value.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
5075
- const rest = value.length - shown.length;
5076
- const oneLine = `[${shown.map((v) => JSON.stringify(v)).join(", ")}]`;
5077
- const rendered = value.length === 0 ? "[]" : rest === 0 && shown.every((v) => primitive(v)) && oneLine.length <= MAX_LINE_CHARS ? oneLine : `[\n${shown.map((v) => truncateLine(`${pad} ${renderModelValue(v, depth + 1, seen)}`)).join(",\n")}${rest > 0 ? `,\n${pad} …(余 ${rest} 项)` : ""}\n${pad}]`;
5078
- seen.delete(value);
5079
- return rendered;
5080
- }
5081
- if (value !== null && typeof value === "object") {
5082
- const entries = Object.entries(value);
5083
- const shown = entries.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
5084
- const rest = entries.length - shown.length;
5085
- const oneLine = `{ ${shown.map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
5086
- const rendered = entries.length === 0 ? "{}" : rest === 0 && shown.every(([, v]) => primitive(v)) && oneLine.length <= MAX_LINE_CHARS ? oneLine : `{\n${shown.map(([k, v]) => truncateLine(`${pad} ${k}: ${renderModelValue(v, depth + 1, seen)}`)).join(",\n")}${rest > 0 ? `,\n${pad} …(余 ${rest} 项)` : ""}\n${pad}}`;
5087
- seen.delete(value);
5088
- return rendered;
5089
- }
5090
- return String(value);
5091
- }
5092
- function primitive(v) {
5093
- return v === null || [
5094
- "string",
5095
- "number",
5096
- "boolean"
5097
- ].includes(typeof v);
5098
- }
5099
- function undefinedValue(value) {
5100
- return typeof value === "object" && value !== null && value.$void === true;
5101
- }
5102
- const TRUNCATION_MAX_BYTES = 51200;
5103
- const TMP_FILE_TTL_MS = 864e5;
5104
- const TMP_FILE_PREFIX = "pi-ptc-output-";
5105
- let sweptTmp = false;
5106
- function sweepExpiredTmp() {
5107
- sweptTmp = true;
5108
- let names;
5109
- try {
5110
- names = readdirSync(tmpdir());
5111
- } catch {
5112
- return;
5113
- }
5114
- for (const name of names) {
5115
- if (!name.startsWith(TMP_FILE_PREFIX)) continue;
5116
- try {
5117
- const file = join(tmpdir(), name);
5118
- if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS) rmSync(file, { force: true });
5119
- } catch {}
5120
- }
5121
- }
5122
- function applyAdr0015Truncation(text) {
5123
- const lines = text.split("\n");
5124
- const bytes = Buffer.byteLength(text, "utf8");
5125
- if (bytes <= 51200 && lines.length <= 2e3) return {
5126
- text,
5127
- truncated: false
5128
- };
5129
- let keptLines = lines.slice(-2e3);
5130
- let kept = keptLines.join("\n");
5131
- while (Buffer.byteLength(kept, "utf8") > 51200 && keptLines.length > 1) {
5132
- keptLines = keptLines.slice(Math.max(1, Math.floor(keptLines.length / 2)));
5133
- kept = keptLines.join("\n");
5134
- }
5135
- const keptBuffer = Buffer.from(kept, "utf8");
5136
- if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES).toString("utf8");
5137
- const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX}${randomUUID()}.txt`);
5138
- mkdirSync(tmpdir(), { recursive: true });
5139
- writeFileSync(fullOutputPath, text, "utf8");
5140
- if (!sweptTmp) sweepExpiredTmp();
5141
- return {
5142
- text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
5143
- truncated: true,
5144
- fullOutputPath
5145
- };
5146
- }
5022
+ // ../output/src/result-text.ts
5147
5023
  function renderToolResult(outcome) {
5148
5024
  let body;
5149
5025
  if (outcome.status === "fulfilled") body = (outcome.logs.length > 0 ? [...outcome.logs, renderModelValue(outcome.value)] : [renderModelValue(outcome.value)]).join("\n");
@@ -5156,7 +5032,7 @@ function renderToolResult(outcome) {
5156
5032
  fullOutputPath
5157
5033
  };
5158
5034
  }
5159
- // ../ptc/src/render.ts
5035
+ // src/ptc-render.ts
5160
5036
  function renderPtcResult(surface) {
5161
5037
  return (result, options, _theme, context) => {
5162
5038
  const args = context.args;
@@ -5175,14 +5051,14 @@ function renderPtcResult(surface) {
5175
5051
  if (options.expanded && !options.isPartial) for (const block of result.content) {
5176
5052
  if (block.type !== "text") continue;
5177
5053
  const lines = block.text.split("\n");
5178
- const kept = lines.slice(0, RENDER_BOUNDS$1.MAX_LOG_LINES_EXPANDED);
5054
+ const kept = lines.slice(0, RENDER_BOUNDS.MAX_LOG_LINES_EXPANDED);
5179
5055
  for (const line of kept) box.addChild(new Text(line));
5180
5056
  if (lines.length > kept.length) box.addChild(new Text(`…(共 ${lines.length} 行,省略 ${lines.length - kept.length} 行)`));
5181
5057
  }
5182
5058
  return box;
5183
5059
  };
5184
5060
  }
5185
- // ../ptc/src/tools.ts
5061
+ // src/ptc-tools.ts
5186
5062
  const RUN_CODE_PARAMETERS = _Object_({
5187
5063
  code: String$1({ description: "要执行的 TypeScript 程序体,可用顶层 await 和 return" }),
5188
5064
  description: String$1({ description: "这段程序要干什么(给人看的一行)" }),
@@ -5297,6 +5173,66 @@ function validateWorkflowArgs(args) {
5297
5173
  throw new Error(`workflow args 必须是 JSON 值:${error instanceof Error ? error.message : String(error)}`);
5298
5174
  }
5299
5175
  }
5176
+ // src/ptc-bindings.ts
5177
+ const KNOWN_BINDING_NAMES = [
5178
+ "read",
5179
+ "bash",
5180
+ "grep",
5181
+ "find",
5182
+ "ls",
5183
+ "edit",
5184
+ "write"
5185
+ ];
5186
+ const OPT_IN_BINDING_NAMES = ["bash"];
5187
+ const DEFAULT_BINDING_NAMES = KNOWN_BINDING_NAMES.filter((name) => !OPT_IN_BINDING_NAMES.includes(name));
5188
+ function resolveBindingNames(activeToolNames, optIns = []) {
5189
+ const allowed = /* @__PURE__ */ new Set([...DEFAULT_BINDING_NAMES, ...optIns]);
5190
+ return KNOWN_BINDING_NAMES.filter((name) => activeToolNames.includes(name) && allowed.has(name));
5191
+ }
5192
+ // src/task-binding.ts
5193
+ const TASK_BINDING_NAME = "pi_task_ptc";
5194
+ const TASK_DOOR_PARAMETERS = _Object_({
5195
+ task: String$1({
5196
+ minLength: 1,
5197
+ description: TASK_DOOR_PARAM_DESCRIPTIONS.task
5198
+ }),
5199
+ agent: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.agent })),
5200
+ why: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.why })),
5201
+ agentScope: Optional(Union([
5202
+ Literal("user"),
5203
+ Literal("project"),
5204
+ Literal("both")
5205
+ ], { description: TASK_DOOR_PARAM_DESCRIPTIONS.agentScope })),
5206
+ model: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.model })),
5207
+ thinkingLevel: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.thinkingLevel })),
5208
+ wait: Optional(Boolean$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.wait })),
5209
+ label: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.label })),
5210
+ readOnly: Optional(Union([Literal("strict"), Literal("lenient")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.readOnly })),
5211
+ timeoutMs: Optional(Number$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.timeoutMs })),
5212
+ projectContext: Optional(Union([Literal("auto"), Literal("none")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.projectContext }))
5213
+ });
5214
+ function createTaskBinding(door) {
5215
+ return {
5216
+ name: TASK_BINDING_NAME,
5217
+ parameters: TASK_DOOR_PARAMETERS,
5218
+ execute(args) {
5219
+ const input = args ?? {};
5220
+ return door.createTask(input.task, {
5221
+ label: input.label,
5222
+ wait: input.wait,
5223
+ why: input.why,
5224
+ model: input.model,
5225
+ thinkingLevel: input.thinkingLevel,
5226
+ agent: input.agent,
5227
+ agentScope: input.agentScope,
5228
+ readOnly: input.readOnly,
5229
+ timeoutMs: input.timeoutMs,
5230
+ projectContext: input.projectContext
5231
+ });
5232
+ }
5233
+ };
5234
+ }
5235
+ const TASK_RECORDS_BINDING_NAME = "task_records";
5300
5236
  // src/session.ts
5301
5237
  function assemblePtcSession(options = {}) {
5302
5238
  const registry = new TaskRegistry(options.onTransition);
@@ -5552,10 +5488,15 @@ function piPtcEntry(pi) {
5552
5488
  }] };
5553
5489
  });
5554
5490
  let toolContext;
5555
- const getBindings = () => createBindingTable(pi, session, () => toolContext);
5491
+ const cwd = process.cwd();
5492
+ const getBindings = () => createBindingTable(pi, session, () => toolContext, {
5493
+ cwd,
5494
+ extCtx: ctx,
5495
+ piVersion
5496
+ });
5556
5497
  registerPtcTools(registerTracked, {
5557
5498
  getBindings,
5558
- cwd: process.cwd(),
5499
+ cwd,
5559
5500
  surface,
5560
5501
  getPool: () => pool,
5561
5502
  onToolContext: (ctx) => {
@@ -5687,13 +5628,52 @@ function normalizeToolValue(value) {
5687
5628
  }
5688
5629
  return value;
5689
5630
  }
5690
- function createBindingTable(pi, session, getToolContext) {
5631
+ const PI_TOOL_FACTORIES = Object.fromEntries([
5632
+ "read",
5633
+ "write",
5634
+ "edit",
5635
+ "grep",
5636
+ "ls",
5637
+ "find"
5638
+ ].map((name) => [name, piHost[`create${name[0].toUpperCase()}${name.slice(1)}ToolDefinition`]]));
5639
+ function hasNestedToolCalls(piVersion) {
5640
+ if (!piVersion) return true;
5641
+ const [major = "0", minor = "0"] = piVersion.trim().replace(/^v/, "").split(".");
5642
+ const M = Number.parseInt(major, 10);
5643
+ const m = Number.parseInt(minor, 10);
5644
+ if (Number.isNaN(M) || Number.isNaN(m)) return true;
5645
+ return M > 0 || M === 0 && m >= 99;
5646
+ }
5647
+ function createBindingTable(pi, session, getToolContext, env) {
5648
+ const { cwd, extCtx, piVersion } = env;
5691
5649
  const active = pi.getActiveTools();
5692
5650
  const known = new Set(KNOWN_BINDING_NAMES);
5693
5651
  const names = resolveBindingNames(!getToolContext() && !active.some((name) => known.has(name)) ? pi.getAllTools().map((tool) => tool.name) : active);
5652
+ let piTools;
5653
+ const piToolFor = (name) => {
5654
+ if (!piTools) {
5655
+ piTools = {};
5656
+ for (const [tool, factory] of Object.entries(PI_TOOL_FACTORIES)) if (typeof factory === "function") piTools[tool] = factory(cwd);
5657
+ }
5658
+ return piTools[name];
5659
+ };
5694
5660
  const table = Object.fromEntries(names.map((name) => [name, async (args, callCtx) => {
5695
5661
  const ctx = getToolContext();
5696
- if (!ctx?.executeTool) throw new Error(`此 pi 版本没有程序内调用工具的通道(需 pi ≥ 0.99,binding: ${name})`);
5662
+ if (!ctx?.executeTool) {
5663
+ const def = hasNestedToolCalls(piVersion) ? void 0 : piToolFor(name);
5664
+ if (!def) throw new Error(`此 pi 版本没有程序内调用工具的通道(需 pi ≥ 0.99,binding: ${name})`);
5665
+ const prepared = def.prepareArguments ? def.prepareArguments(args) : args;
5666
+ const result = await def.execute(`ptc-${name}`, prepared, callCtx.signal, void 0, extCtx);
5667
+ for (const block of result.content ?? []) if (block.type === "image") callCtx.emitImage(block);
5668
+ if (result.isError) {
5669
+ const text = (result.content ?? []).map((block) => {
5670
+ const obj = block;
5671
+ return obj.type === "text" ? obj.text ?? "" : `[${obj.type ?? "block"}]`;
5672
+ }).join("\n");
5673
+ throw new Error(text || `工具 ${name} 执行失败`);
5674
+ }
5675
+ return normalizeToolValue(result.structuredContent ?? result.content);
5676
+ }
5697
5677
  const outcome = await ctx.executeTool(name, args, { signal: callCtx.signal });
5698
5678
  for (const block of outcome.result.content) if (block.type === "image") callCtx.emitImage(block);
5699
5679
  if (outcome.isError) {
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { n as runPtcProgram, t as createFsBindings } from "./fs-bindings-DCGErhvt.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ // src/cli.ts
6
+ const USAGE = `用法:ptc <program.ts> [--workdir <dir>] [--timeout <ms>] [--json]
7
+
8
+ 程序在运行格里跑(看不见 process / fetch / 动态 import),能调的只有文件四件套
9
+ (read / write / grep / glob / edit),且只在 workdir 内活动。`;
10
+ function parseArgs(argv) {
11
+ let program = "";
12
+ let workdir = process.cwd();
13
+ let timeoutMs;
14
+ let json = false;
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const arg = argv[i];
17
+ if (arg === "--json") json = true;
18
+ else if (arg === "--workdir" || arg === "-w") workdir = argv[++i] ?? workdir;
19
+ else if (arg === "--timeout") timeoutMs = Number(argv[++i]);
20
+ else if (arg.startsWith("-")) throw new Error(`未知参数 ${arg}`);
21
+ else if (program) throw new Error(`只能跑一个程序(多给了 ${arg})`);
22
+ else program = arg;
23
+ }
24
+ if (!program) throw new Error(USAGE);
25
+ if (timeoutMs !== void 0 && !Number.isFinite(timeoutMs)) throw new Error("--timeout 要一个毫秒数");
26
+ return {
27
+ program,
28
+ workdir: resolve(workdir),
29
+ timeoutMs,
30
+ json
31
+ };
32
+ }
33
+ function isVoid(value) {
34
+ return typeof value === "object" && value !== null && "$void" in value;
35
+ }
36
+ async function main() {
37
+ const argv = process.argv.slice(2);
38
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
39
+ console.log(USAGE);
40
+ return;
41
+ }
42
+ const options = parseArgs(argv);
43
+ const outcome = await runPtcProgram({
44
+ code: readFileSync(options.program, "utf8"),
45
+ cwd: options.workdir,
46
+ timeoutMs: options.timeoutMs,
47
+ bindings: createFsBindings(options.workdir)
48
+ });
49
+ for (const line of outcome.logs) console.error(line);
50
+ if (options.json) console.log(JSON.stringify(outcome, null, 2));
51
+ else if (outcome.status === "fulfilled") console.log(isVoid(outcome.value) ? "(程序无返回值)" : JSON.stringify(outcome.value, null, 2));
52
+ if (outcome.status !== "fulfilled") {
53
+ console.error(outcome.status === "canceled" ? outcome.reason : outcome.error);
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+ main().catch((error) => {
58
+ console.error(error instanceof Error ? error.message : String(error));
59
+ process.exitCode = 1;
60
+ });
61
+ export {};
@@ -0,0 +1,423 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { existsSync, globSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import { basename, dirname, resolve, sep } from "node:path";
5
+ // src/limits.ts
6
+ const DEFAULT_LIMITS = {
7
+ timeoutMs: 12e4,
8
+ maxTimeoutMs: 6e5,
9
+ graceMs: 3e3,
10
+ maxOutputBytes: 67108864,
11
+ maxPendingCalls: 128,
12
+ maxParallelSubCalls: 10,
13
+ poolSize: 4,
14
+ poolAcquireTimeoutMs: 3e4
15
+ };
16
+ function effectiveTimeoutMs(requested) {
17
+ if (requested === void 0) return DEFAULT_LIMITS.timeoutMs;
18
+ return Math.min(Math.max(requested, 1e3), DEFAULT_LIMITS.maxTimeoutMs);
19
+ }
20
+ // src/sub-calls.ts
21
+ const PREVIEW_MAX_CHARS = 60;
22
+ var SubCallTracker = class {
23
+ #records = /* @__PURE__ */ new Map();
24
+ #order = [];
25
+ start(callId, name, args) {
26
+ let preview = "";
27
+ try {
28
+ preview = JSON.stringify(args) ?? "";
29
+ } catch {
30
+ preview = String(args);
31
+ }
32
+ this.#records.set(callId, {
33
+ callId,
34
+ name,
35
+ argsPreview: preview.slice(0, PREVIEW_MAX_CHARS),
36
+ status: "running",
37
+ startedAt: performance.now()
38
+ });
39
+ this.#order.push(callId);
40
+ }
41
+ end(callId, status) {
42
+ const record = this.#records.get(callId);
43
+ if (!record) return;
44
+ record.status = status;
45
+ record.durationMs = Math.round(performance.now() - record.startedAt);
46
+ }
47
+ snapshot() {
48
+ return this.#order.map((callId) => this.#records.get(callId));
49
+ }
50
+ };
51
+ // src/worker-source.ts
52
+ function buildWorkerUrl(from) {
53
+ const built = new URL("./worker.js", from);
54
+ if (existsSync(built)) return built;
55
+ return new URL("../src/worker-entry.ts", from);
56
+ }
57
+ // src/dispatcher.ts
58
+ const WORKER_ENV_ALLOW_LIST = [
59
+ "PATH",
60
+ "PATHEXT",
61
+ "SYSTEMROOT",
62
+ "WINDIR",
63
+ "TEMP",
64
+ "TMP"
65
+ ];
66
+ function workerEnv() {
67
+ return Object.fromEntries(WORKER_ENV_ALLOW_LIST.filter((key) => key in process.env).map((key) => [key, process.env[key]]));
68
+ }
69
+ function spawnPtcWorker() {
70
+ return new Worker(buildWorkerUrl(import.meta.url), {
71
+ env: workerEnv(),
72
+ execArgv: [],
73
+ resourceLimits: {
74
+ maxOldGenerationSizeMb: 512,
75
+ maxYoungGenerationSizeMb: 64
76
+ }
77
+ });
78
+ }
79
+ async function runPtcProgram(request) {
80
+ const timeoutMs = effectiveTimeoutMs(request.timeoutMs);
81
+ const cwd = request.cwd ?? process.cwd();
82
+ const worker = request.pool ? await request.pool.acquire() : spawnPtcWorker();
83
+ return new Promise((resolve) => {
84
+ const runId = crypto.randomUUID();
85
+ const logs = [];
86
+ const images = [];
87
+ const subCalls = new SubCallTracker();
88
+ let pendingCalls = 0;
89
+ let runningCalls = 0;
90
+ const callQueue = [];
91
+ const controllers = /* @__PURE__ */ new Set();
92
+ let settled = false;
93
+ let waitingReset = false;
94
+ let resetTimer;
95
+ const send = (frame) => worker.postMessage(frame);
96
+ const finish = (outcome) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ clearTimeout(timer);
100
+ callQueue.length = 0;
101
+ for (const abort of controllers) abort.abort();
102
+ const withExtras = {
103
+ ...outcome,
104
+ subCalls: subCalls.snapshot()
105
+ };
106
+ if (withExtras.status === "fulfilled" && images.length > 0) withExtras.images = images;
107
+ resolve(withExtras);
108
+ if (request.pool) {
109
+ waitingReset = true;
110
+ resetTimer = setTimeout(() => {
111
+ if (!waitingReset) return;
112
+ waitingReset = false;
113
+ request.pool.discard(worker);
114
+ }, DEFAULT_LIMITS.graceMs);
115
+ resetTimer.unref();
116
+ send({ kind: "reset" });
117
+ } else worker.terminate();
118
+ };
119
+ const timer = setTimeout(() => {
120
+ send({
121
+ kind: "cancel",
122
+ id: runId,
123
+ reason: "timeout"
124
+ });
125
+ finish({
126
+ status: "canceled",
127
+ reason: `timeout after ${timeoutMs}ms`,
128
+ logs
129
+ });
130
+ }, timeoutMs);
131
+ const onMessage = (frame) => {
132
+ if (settled && frame.kind !== "ready") return;
133
+ if (frame.kind === "ready") {
134
+ if (waitingReset) {
135
+ waitingReset = false;
136
+ if (resetTimer) clearTimeout(resetTimer);
137
+ worker.off("message", onMessage);
138
+ worker.off("error", onError);
139
+ worker.off("exit", onExit);
140
+ request.pool.release(worker);
141
+ }
142
+ return;
143
+ }
144
+ if (frame.runId !== runId) return;
145
+ if (frame.kind === "log") {
146
+ logs.push(frame.text);
147
+ return;
148
+ }
149
+ if (frame.kind === "call") {
150
+ if (pendingCalls >= DEFAULT_LIMITS.maxPendingCalls) {
151
+ send({
152
+ kind: "result",
153
+ callId: frame.callId,
154
+ ok: false,
155
+ error: `maxPendingCalls ${DEFAULT_LIMITS.maxPendingCalls} reached`
156
+ });
157
+ return;
158
+ }
159
+ pendingCalls++;
160
+ subCalls.start(frame.callId, frame.name, frame.args);
161
+ const execute = async () => {
162
+ if (settled) return;
163
+ const controller = new AbortController();
164
+ controllers.add(controller);
165
+ try {
166
+ const binding = request.bindings[frame.name];
167
+ if (!binding) {
168
+ subCalls.end(frame.callId, "rejected");
169
+ send({
170
+ kind: "result",
171
+ callId: frame.callId,
172
+ ok: false,
173
+ error: `unknown binding ${frame.name}`
174
+ });
175
+ return;
176
+ }
177
+ const value = await binding(frame.args, {
178
+ cwd,
179
+ emitImage: (image) => images.push(image),
180
+ signal: controller.signal
181
+ });
182
+ subCalls.end(frame.callId, "ok");
183
+ send({
184
+ kind: "result",
185
+ callId: frame.callId,
186
+ ok: true,
187
+ value: value === void 0 ? null : value
188
+ });
189
+ } catch (error) {
190
+ subCalls.end(frame.callId, "error");
191
+ send({
192
+ kind: "result",
193
+ callId: frame.callId,
194
+ ok: false,
195
+ error: error instanceof Error ? error.message : String(error)
196
+ });
197
+ } finally {
198
+ controllers.delete(controller);
199
+ pendingCalls--;
200
+ runningCalls--;
201
+ const next = callQueue.shift();
202
+ if (next) {
203
+ runningCalls++;
204
+ next();
205
+ }
206
+ }
207
+ };
208
+ if (runningCalls < DEFAULT_LIMITS.maxParallelSubCalls) {
209
+ runningCalls++;
210
+ execute();
211
+ } else callQueue.push(execute);
212
+ return;
213
+ }
214
+ if (frame.kind === "settled") {
215
+ const outcome = frame.outcome;
216
+ if (JSON.stringify({
217
+ logs: outcome.logs,
218
+ value: outcome.status === "fulfilled" ? outcome.value : void 0
219
+ }).length > DEFAULT_LIMITS.maxOutputBytes) {
220
+ finish({
221
+ status: "rejected",
222
+ error: `output exceeds ${DEFAULT_LIMITS.maxOutputBytes} bytes budget`,
223
+ logs
224
+ });
225
+ return;
226
+ }
227
+ finish(outcome);
228
+ }
229
+ };
230
+ worker.on("message", onMessage);
231
+ const onError = (error) => void finish({
232
+ status: "rejected",
233
+ error: `worker error: ${error instanceof Error ? error.message : String(error)}`,
234
+ logs
235
+ });
236
+ const onExit = (code) => {
237
+ if (!settled) finish({
238
+ status: "rejected",
239
+ error: `worker exited early with code ${code}`,
240
+ logs
241
+ });
242
+ };
243
+ worker.on("error", onError);
244
+ worker.on("exit", onExit);
245
+ send({
246
+ kind: "init",
247
+ surface: request.surface ?? "run_code",
248
+ bindingNames: Object.keys(request.bindings)
249
+ });
250
+ send({
251
+ kind: "run",
252
+ id: runId,
253
+ code: request.code,
254
+ args: request.args ?? {}
255
+ });
256
+ });
257
+ }
258
+ // src/fs-bindings.ts
259
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
260
+ const SKIP_SUFFIXES = /* @__PURE__ */ new Set([
261
+ ".png",
262
+ ".jpg",
263
+ ".jpeg",
264
+ ".gif",
265
+ ".webp",
266
+ ".ico",
267
+ ".zip",
268
+ ".gz",
269
+ ".tgz",
270
+ ".pdf",
271
+ ".woff",
272
+ ".woff2",
273
+ ".map"
274
+ ]);
275
+ const GREP_MAX_RESULTS = 500;
276
+ const GLOB_MAX_RESULTS = 1e3;
277
+ const READ_MAX_BYTES = 2097152;
278
+ const WRITE_MAX_BYTES = 2097152;
279
+ function strArg(args, key) {
280
+ const value = args?.[key];
281
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${key} 必须是非空字符串`);
282
+ return value;
283
+ }
284
+ function jailed(workdirReal, p) {
285
+ const abs = resolve(workdirReal, p);
286
+ let real;
287
+ try {
288
+ real = realpathSync(abs);
289
+ } catch {
290
+ real = resolve(realpathSync(dirname(abs)), basename(abs));
291
+ }
292
+ if (real !== workdirReal && !real.startsWith(workdirReal + sep)) throw new Error(`path escapes workdir: ${p}`);
293
+ return real;
294
+ }
295
+ function* walk(dir) {
296
+ let entries;
297
+ try {
298
+ entries = readdirSync(dir, { withFileTypes: true });
299
+ } catch {
300
+ return;
301
+ }
302
+ for (const entry of entries) if (entry.isDirectory()) {
303
+ if (SKIP_DIRS.has(entry.name)) continue;
304
+ yield* walk(resolve(dir, entry.name));
305
+ } else if (entry.isFile()) yield resolve(dir, entry.name);
306
+ }
307
+ function readPlainText(file) {
308
+ const stat = statSync(file);
309
+ const dot = file.lastIndexOf(".");
310
+ const suffix = dot === -1 ? "" : file.slice(dot).toLowerCase();
311
+ if (stat.size > READ_MAX_BYTES || SKIP_SUFFIXES.has(suffix)) return void 0;
312
+ const text = readFileSync(file, "utf8");
313
+ return text.includes("�") ? void 0 : text;
314
+ }
315
+ function createFsBindings(workdir, onCall) {
316
+ let workdirReal;
317
+ try {
318
+ workdirReal = realpathSync(resolve(workdir));
319
+ } catch {
320
+ throw new Error(`workdir 不存在或不可达: ${workdir}(workdir 传用户工作区绝对路径)`);
321
+ }
322
+ const table = {
323
+ read: async (args) => {
324
+ const real = jailed(workdirReal, strArg(args, "path"));
325
+ const stat = statSync(real);
326
+ if (!stat.isFile()) throw new Error(`not a file: ${strArg(args, "path")}`);
327
+ if (stat.size > READ_MAX_BYTES) throw new Error(`file too large (${stat.size} bytes > ${READ_MAX_BYTES})`);
328
+ return readFileSync(real, "utf8");
329
+ },
330
+ write: async (args) => {
331
+ const p = strArg(args, "path");
332
+ const content = strArg(args, "content");
333
+ const bytes = Buffer.byteLength(content, "utf8");
334
+ if (bytes > WRITE_MAX_BYTES) throw new Error(`content too large (${bytes} bytes > ${WRITE_MAX_BYTES} bytes)`);
335
+ const abs = resolve(workdirReal, p);
336
+ const parentAbs = dirname(abs);
337
+ if (!existsSync(parentAbs)) throw new Error(`parent dir does not exist: ${dirname(p)}(write 不隐式建目录)`);
338
+ const parentReal = jailed(workdirReal, parentAbs);
339
+ const real = resolve(parentReal, basename(abs));
340
+ const tmp = `${real}.ptc-${randomUUID()}`;
341
+ writeFileSync(tmp, content, "utf8");
342
+ renameSync(tmp, real);
343
+ return { written: real.slice(workdirReal.length + 1) };
344
+ },
345
+ grep: async (args) => {
346
+ const pattern = strArg(args, "pattern");
347
+ const scope = typeof args?.path === "string" ? jailed(workdirReal, strArg(args, "path")) : workdirReal;
348
+ let regex;
349
+ try {
350
+ regex = new RegExp(pattern);
351
+ } catch {
352
+ throw new Error(`invalid pattern: ${pattern}`);
353
+ }
354
+ const hits = [];
355
+ const files = statSync(scope).isFile() ? [scope] : walk(scope);
356
+ outer: for (const file of files) {
357
+ let text;
358
+ try {
359
+ text = readPlainText(file);
360
+ } catch {
361
+ continue;
362
+ }
363
+ if (text === void 0) continue;
364
+ const lines = text.split("\n");
365
+ for (let i = 0; i < lines.length; i++) if (regex.test(lines[i])) {
366
+ hits.push(`${file.slice(workdirReal.length + 1)}:${i + 1}: ${lines[i]}`);
367
+ if (hits.length >= GREP_MAX_RESULTS) {
368
+ hits.push(`…(已截断:达到 ${GREP_MAX_RESULTS} 条上限)`);
369
+ break outer;
370
+ }
371
+ }
372
+ }
373
+ return hits;
374
+ },
375
+ glob: async (args) => {
376
+ const pattern = strArg(args, "pattern");
377
+ let found;
378
+ try {
379
+ found = globSync(pattern, { cwd: workdirReal });
380
+ } catch {
381
+ throw new Error(`invalid glob: ${pattern}`);
382
+ }
383
+ return found.slice(0, GLOB_MAX_RESULTS);
384
+ },
385
+ edit: async (args) => {
386
+ const real = jailed(workdirReal, strArg(args, "path"));
387
+ const oldString = strArg(args, "old_string");
388
+ const newString = strArg(args, "new_string");
389
+ const text = readFileSync(real, "utf8");
390
+ const count = text.split(oldString).length - 1;
391
+ if (count === 0) throw new Error("old_string not found");
392
+ if (count > 1) throw new Error(`old_string matches ${count} places — must be unique`);
393
+ const tmp = `${real}.ptc-${randomUUID()}`;
394
+ writeFileSync(tmp, text.replace(oldString, newString), "utf8");
395
+ renameSync(tmp, real);
396
+ return { edited: real.slice(workdirReal.length + 1) };
397
+ }
398
+ };
399
+ if (!onCall) return table;
400
+ return {
401
+ read: (a) => {
402
+ onCall();
403
+ return table.read(a);
404
+ },
405
+ write: (a) => {
406
+ onCall();
407
+ return table.write(a);
408
+ },
409
+ grep: (a) => {
410
+ onCall();
411
+ return table.grep(a);
412
+ },
413
+ glob: (a) => {
414
+ onCall();
415
+ return table.glob(a);
416
+ },
417
+ edit: (a) => {
418
+ onCall();
419
+ return table.edit(a);
420
+ }
421
+ };
422
+ }
423
+ export { DEFAULT_LIMITS as i, runPtcProgram as n, spawnPtcWorker as r, createFsBindings as t };
@@ -0,0 +1,77 @@
1
+ import { i as DEFAULT_LIMITS, n as runPtcProgram, r as spawnPtcWorker, t as createFsBindings } from "./fs-bindings-DCGErhvt.js";
2
+ // src/pool.ts
3
+ var WorkerPool = class {
4
+ #spawn;
5
+ #capacity;
6
+ #idle = [];
7
+ #waiters = [];
8
+ #retired = /* @__PURE__ */ new WeakSet();
9
+ #spawned = 0;
10
+ #draining = false;
11
+ constructor(spawn, capacity = DEFAULT_LIMITS.poolSize) {
12
+ this.#spawn = spawn;
13
+ this.#capacity = capacity;
14
+ }
15
+ acquire(timeoutMs = DEFAULT_LIMITS.poolAcquireTimeoutMs) {
16
+ const idle = this.#idle.pop();
17
+ if (idle) return Promise.resolve(idle);
18
+ if (this.#spawned < this.#capacity) {
19
+ this.#draining = false;
20
+ this.#spawned++;
21
+ return Promise.resolve(this.#track(this.#spawn()));
22
+ }
23
+ return new Promise((resolve, reject) => {
24
+ const timer = setTimeout(() => {
25
+ const index = this.#waiters.findIndex((w) => w.timer === timer);
26
+ if (index >= 0) this.#waiters.splice(index, 1);
27
+ reject(/* @__PURE__ */ new Error(`worker acquire timed out after ${timeoutMs}ms`));
28
+ }, timeoutMs);
29
+ this.#waiters.push({
30
+ resolve,
31
+ timer
32
+ });
33
+ });
34
+ }
35
+ release(worker) {
36
+ if (this.#draining) {
37
+ this.#retire(worker);
38
+ worker.terminate();
39
+ return;
40
+ }
41
+ const waiter = this.#waiters.shift();
42
+ if (waiter) {
43
+ clearTimeout(waiter.timer);
44
+ waiter.resolve(worker);
45
+ return;
46
+ }
47
+ this.#idle.push(worker);
48
+ }
49
+ /** 被杀/卡死/崩溃的 worker:收回名额并摘出空闲表(terminate 幂等) */
50
+ discard(worker) {
51
+ this.#retire(worker);
52
+ const index = this.#idle.indexOf(worker);
53
+ if (index >= 0) this.#idle.splice(index, 1);
54
+ worker.terminate();
55
+ }
56
+ drain() {
57
+ this.#draining = true;
58
+ for (const worker of this.#idle.splice(0)) {
59
+ this.#retire(worker);
60
+ worker.terminate();
61
+ }
62
+ }
63
+ #track(worker) {
64
+ worker.once("exit", () => {
65
+ this.#retire(worker);
66
+ const index = this.#idle.indexOf(worker);
67
+ if (index >= 0) this.#idle.splice(index, 1);
68
+ });
69
+ return worker;
70
+ }
71
+ #retire(worker) {
72
+ if (this.#retired.has(worker)) return;
73
+ this.#retired.add(worker);
74
+ this.#spawned = Math.max(0, this.#spawned - 1);
75
+ }
76
+ };
77
+ export { DEFAULT_LIMITS, WorkerPool, createFsBindings, runPtcProgram, spawnPtcWorker };
@@ -0,0 +1,197 @@
1
+ import { stripTypeScriptTypes } from "node:module";
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import vm from "node:vm";
4
+ // src/compile.ts
5
+ function compileProgram(source) {
6
+ return `"use strict";\n${stripTypeScriptTypes(`async function __ptcProgram() {\n${source}\n}`, { mode: "strip" })}\n;globalThis.__ptcProgram = __ptcProgram;`;
7
+ }
8
+ // src/worker-main.ts
9
+ const CELL_PREAMBLE = `"use strict";
10
+ (function () {
11
+ var __bridge = globalThis.__ptcCall;
12
+ var __emit = globalThis.__ptcConsole;
13
+ var __args = JSON.parse(globalThis.__ptcArgsJson);
14
+ // contextified 全局属性不可 delete(vm 限制)但可写——置 undefined 即脱手,桥只剩序言闭包可达
15
+ globalThis.__ptcCall = undefined;
16
+ globalThis.__ptcConsole = undefined;
17
+ globalThis.__ptcArgsJson = undefined;
18
+ function __ptcFmt(v) {
19
+ if (typeof v === "string") return v;
20
+ try { var s = JSON.stringify(v); return s === undefined ? String(v) : s; } catch (e) { return String(v); }
21
+ }
22
+ function __ptcJoin(values) {
23
+ var out = [];
24
+ for (var i = 0; i < values.length; i++) out.push(__ptcFmt(values[i]));
25
+ return out.join(" ");
26
+ }
27
+ var __ptcTools = {};
28
+ __PTC_NAMES__.forEach(function (name) {
29
+ __ptcTools[name] = function (args) {
30
+ return new Promise(function (resolve, reject) {
31
+ __bridge(name, JSON.stringify(args === undefined ? null : args)).then(function (json) {
32
+ var value;
33
+ try { value = JSON.parse(json); } catch (e) { reject(e); return; }
34
+ resolve(value);
35
+ }, reject);
36
+ });
37
+ };
38
+ });
39
+ globalThis.tools = Object.freeze(__ptcTools);
40
+ globalThis.console = Object.freeze({
41
+ log: function () { __emit("log", __ptcJoin(arguments)); },
42
+ info: function () { __emit("info", __ptcJoin(arguments)); },
43
+ warn: function () { __emit("warn", __ptcJoin(arguments)); },
44
+ error: function () { __emit("error", __ptcJoin(arguments)); },
45
+ debug: function () { __emit("debug", __ptcJoin(arguments)); }
46
+ });
47
+ globalThis.args = Object.freeze(__args);
48
+ if (__PTC_WORKFLOW__) {
49
+ globalThis.workflow = Object.freeze({
50
+ log: function () { __emit("log", __ptcJoin(arguments)); },
51
+ phase: function (name) { __emit("log", "— phase: " + name + " —"); },
52
+ parallel: function (items) { return Promise.allSettled(items.map(function (run) { return run(); })); },
53
+ pipeline: function (items, step) {
54
+ var results = [];
55
+ return items.reduce(function (chain, item, index) {
56
+ return chain.then(function () { return Promise.resolve(step(item, index)).then(function (r) { results.push(r); }); });
57
+ }, Promise.resolve()).then(function () { return results; });
58
+ }
59
+ });
60
+ }
61
+ })();
62
+ `;
63
+ function workerMain(deps) {
64
+ let bindingNames = [];
65
+ let surface = "run_code";
66
+ let pending = /* @__PURE__ */ new Map();
67
+ let nextCallId = 1;
68
+ let currentRunId = null;
69
+ let logs = [];
70
+ deps.onFrame((frame) => {
71
+ if (frame.kind === "init") {
72
+ bindingNames = frame.bindingNames;
73
+ surface = frame.surface;
74
+ deps.post({ kind: "ready" });
75
+ return;
76
+ }
77
+ if (frame.kind === "reset") {
78
+ currentRunId = null;
79
+ for (const { reject } of pending.values()) reject(/* @__PURE__ */ new Error("run reset:worker 被复用"));
80
+ pending = /* @__PURE__ */ new Map();
81
+ deps.post({ kind: "ready" });
82
+ return;
83
+ }
84
+ if (frame.kind === "result") {
85
+ const entry = pending.get(frame.callId);
86
+ if (!entry) return;
87
+ pending.delete(frame.callId);
88
+ if (frame.ok) {
89
+ let json;
90
+ try {
91
+ json = JSON.stringify(frame.value ?? null);
92
+ } catch {
93
+ json = void 0;
94
+ }
95
+ if (json === void 0) entry.reject(/* @__PURE__ */ new Error("binding 返回值不可 JSON 序列化(如循环引用)"));
96
+ else entry.resolve(json);
97
+ } else entry.reject(new Error(frame.error));
98
+ return;
99
+ }
100
+ if (frame.kind === "cancel") return;
101
+ run(frame.code, frame.id, frame.args);
102
+ });
103
+ function preamble() {
104
+ return CELL_PREAMBLE.replace(/__PTC_NAMES__/g, () => JSON.stringify(bindingNames)).replace(/__PTC_WORKFLOW__/g, () => surface === "workflow" ? "true" : "false");
105
+ }
106
+ async function run(code, runId, args) {
107
+ currentRunId = runId;
108
+ logs = [];
109
+ const postLog = (level, text) => {
110
+ logs.push(text);
111
+ deps.post({
112
+ kind: "log",
113
+ runId,
114
+ level,
115
+ text
116
+ });
117
+ };
118
+ const outcome = await execute(code, vm.createContext({
119
+ __ptcArgsJson: JSON.stringify(args ?? {}),
120
+ __ptcCall(name, argsJson) {
121
+ if (currentRunId === null) return Promise.reject(/* @__PURE__ */ new Error("run 已结束:reset 后不再接受工具调用"));
122
+ let parsed = null;
123
+ try {
124
+ parsed = JSON.parse(argsJson);
125
+ } catch {
126
+ return Promise.reject(/* @__PURE__ */ new Error("工具参数不是合法 JSON"));
127
+ }
128
+ const call = new Promise((resolve, reject) => {
129
+ const callId = nextCallId++;
130
+ pending.set(callId, {
131
+ resolve,
132
+ reject
133
+ });
134
+ deps.post({
135
+ kind: "call",
136
+ runId: currentRunId ?? "",
137
+ callId,
138
+ name,
139
+ args: parsed
140
+ });
141
+ });
142
+ call.catch(() => {});
143
+ return call;
144
+ },
145
+ __ptcConsole(level, text) {
146
+ postLog(level, String(text));
147
+ }
148
+ }));
149
+ deps.post({
150
+ kind: "settled",
151
+ runId,
152
+ outcome
153
+ });
154
+ }
155
+ function errorMessage(error) {
156
+ if (error instanceof Error) return error.message;
157
+ if (error && typeof error === "object" && typeof error.message === "string") return error.message;
158
+ return String(error);
159
+ }
160
+ async function execute(code, context) {
161
+ try {
162
+ vm.runInContext(preamble(), context, { filename: "ptc_preamble.js" });
163
+ vm.runInContext(compileProgram(code), context, { filename: "run_code.js" });
164
+ const value = await context.__ptcProgram.call(context);
165
+ if (value === void 0) return {
166
+ status: "fulfilled",
167
+ value: { $void: true },
168
+ logs
169
+ };
170
+ const json = JSON.stringify(value);
171
+ if (json === void 0) return {
172
+ status: "rejected",
173
+ error: "程序返回值不可序列化",
174
+ logs
175
+ };
176
+ return {
177
+ status: "fulfilled",
178
+ value: JSON.parse(json),
179
+ logs
180
+ };
181
+ } catch (error) {
182
+ return {
183
+ status: "rejected",
184
+ error: errorMessage(error),
185
+ logs
186
+ };
187
+ }
188
+ }
189
+ }
190
+ // src/worker-entry.ts
191
+ if (!parentPort) throw new Error("worker-entry 只能在 worker 线程里加载");
192
+ Object.freeze(workerData);
193
+ workerMain({
194
+ post: (frame) => parentPort.postMessage(frame),
195
+ onFrame: (handler) => parentPort.on("message", (frame) => handler(frame))
196
+ });
197
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ptc-subagents",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Programmable tool calling (PTC) and subagent fan-out for pi",
5
5
  "keywords": [
6
6
  "pi",
@@ -18,6 +18,9 @@
18
18
  "type": "git",
19
19
  "url": "git+https://github.com/a1121611810/pi-ptc-subagents.git"
20
20
  },
21
+ "bin": {
22
+ "ptc": "./dist/ptc/cli.js"
23
+ },
21
24
  "files": [
22
25
  "dist/**/*.js",
23
26
  "docs/routes/**/*.md",
@@ -27,18 +30,20 @@
27
30
  "type": "module",
28
31
  "main": "./dist/index.js",
29
32
  "exports": {
30
- ".": "./dist/index.js"
33
+ ".": "./dist/index.js",
34
+ "./ptc": "./dist/ptc/index.js"
31
35
  },
32
36
  "scripts": {
33
37
  "build": "pnpm -r --filter=!@pi-ptc/website run build",
34
38
  "build:kimi": "pnpm --filter @pi-ptc/kimi-mcp run build && node scripts/assemble-kimi.mjs",
35
- "prepack": "pnpm run build && rm -rf dist docs/routes && cp -R packages/entry/dist dist && cp -R packages/routes/docs/routes docs/routes",
39
+ "prepack": "pnpm run build && rm -rf dist docs/routes && cp -R packages/entry/dist dist && cp -R packages/ptc/dist dist/ptc && cp -R packages/routes/docs/routes docs/routes",
36
40
  "check": "pnpm -r --filter=!@pi-ptc/website run check",
37
41
  "lint": "oxlint",
38
- "test": "pnpm run test:pi && pnpm run test:output && pnpm run test:kimi",
42
+ "test": "pnpm run test:pi && pnpm run test:output && pnpm run test:kimi && pnpm run test:standalone",
39
43
  "test:pi": "node --test tests/pi/*.test.mjs",
40
44
  "test:output": "node --test tests/output/*.test.mjs",
41
45
  "test:kimi": "node --test tests/kimi/*.test.mjs",
46
+ "test:standalone": "node --test tests/standalone/*.test.mjs",
42
47
  "pretest": "pnpm run build",
43
48
  "fmt": "oxfmt",
44
49
  "fmt:check": "oxfmt --check"