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 +37 -3
- package/dist/index.js +181 -201
- package/dist/ptc/cli.js +61 -0
- package/dist/ptc/fs-bindings-DCGErhvt.js +423 -0
- package/dist/ptc/index.js +77 -0
- package/dist/ptc/worker.js +197 -0
- package/package.json +9 -4
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
|
|
132
|
-
still
|
|
133
|
-
|
|
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
|
|
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
|
|
3916
|
-
function stripAnsi
|
|
3917
|
-
return text.replace(ANSI_PATTERN
|
|
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
|
|
3920
|
-
return stripAnsi
|
|
3920
|
+
function sanitizeText(text) {
|
|
3921
|
+
return stripAnsi(text.replace(/\r/g, ""));
|
|
3921
3922
|
}
|
|
3922
|
-
const MAX_LINE_CHARS
|
|
3923
|
-
function truncateLine
|
|
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
|
|
3931
|
-
const TMP_FILE_TTL_MS
|
|
3932
|
-
const TMP_FILE_PREFIX
|
|
3933
|
-
let sweptTmp
|
|
3934
|
-
function sweepExpiredTmp
|
|
3935
|
-
sweptTmp
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3965
|
-
const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX
|
|
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
|
|
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
|
|
4031
|
+
...record.label ? { label: truncateLine(record.label, LABEL_MAX_CHARS) } : {},
|
|
3992
4032
|
...record.finishedAt !== void 0 ? { finishedAt: record.finishedAt } : {},
|
|
3993
|
-
...record.task ? { task: truncateLine
|
|
3994
|
-
...record.report?.summary ? { summary: truncateLine
|
|
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
|
|
4293
|
+
if (answer) return { text: applyAdr0015Truncation(answer).text };
|
|
4254
4294
|
return {
|
|
4255
|
-
output: applyAdr0015Truncation
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
// ../
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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) {
|
package/dist/ptc/cli.js
ADDED
|
@@ -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
|
+
"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"
|