pi-ptc-subagents 7.2.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 +829 -623
- 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/docs/routes/create-a-task.md +3 -0
- package/docs/routes/manage-a-task.md +57 -3
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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";
|
|
4
5
|
import { execSync, spawn } from "node:child_process";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
7
|
+
import { mkdir, open, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
7
8
|
import { gunzipSync, gzip } from "node:zlib";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -2746,6 +2747,15 @@ const TERMINAL_STATUSES = [
|
|
|
2746
2747
|
"canceled",
|
|
2747
2748
|
"lost"
|
|
2748
2749
|
];
|
|
2750
|
+
const TASK_STATUSES = [
|
|
2751
|
+
"queued",
|
|
2752
|
+
"running",
|
|
2753
|
+
"stopping",
|
|
2754
|
+
"succeeded",
|
|
2755
|
+
"failed",
|
|
2756
|
+
"canceled",
|
|
2757
|
+
"lost"
|
|
2758
|
+
];
|
|
2749
2759
|
function isTerminal(status) {
|
|
2750
2760
|
return TERMINAL_STATUSES.includes(status);
|
|
2751
2761
|
}
|
|
@@ -3341,6 +3351,47 @@ var NotificationCenter = class {
|
|
|
3341
3351
|
function isTerminalStatus(status) {
|
|
3342
3352
|
return status === "succeeded" || status === "failed" || status === "canceled" || status === "lost";
|
|
3343
3353
|
}
|
|
3354
|
+
/** 护栏的检查间隔(秒级问一次就够——阈值本身是分钟量级) */
|
|
3355
|
+
const STALL_CHECK_INTERVAL_MS = 1e3;
|
|
3356
|
+
var ChildActivityTracker = class {
|
|
3357
|
+
#buffer = "";
|
|
3358
|
+
#lastActivityAt;
|
|
3359
|
+
/** 在飞的工具调用计数(嵌套调用靠计数,不靠集合——start/end 成对进出) */
|
|
3360
|
+
#toolOpen = 0;
|
|
3361
|
+
#now;
|
|
3362
|
+
constructor(now = Date.now) {
|
|
3363
|
+
this.#now = now;
|
|
3364
|
+
this.#lastActivityAt = now();
|
|
3365
|
+
}
|
|
3366
|
+
/** 喂一段 stdout 增量(chunk 边界不按行切 ⇒ 自己攒半行) */
|
|
3367
|
+
push(chunk) {
|
|
3368
|
+
this.#lastActivityAt = this.#now();
|
|
3369
|
+
this.#buffer += chunk;
|
|
3370
|
+
let index = this.#buffer.indexOf("\n");
|
|
3371
|
+
while (index >= 0) {
|
|
3372
|
+
this.#consume(this.#buffer.slice(0, index));
|
|
3373
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
3374
|
+
index = this.#buffer.indexOf("\n");
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
/** 两条都成立 ⇒ 孩子什么都没在做 */
|
|
3378
|
+
isStalled(now, thresholdMs) {
|
|
3379
|
+
if (this.#toolOpen > 0) return false;
|
|
3380
|
+
return now - this.#lastActivityAt >= thresholdMs;
|
|
3381
|
+
}
|
|
3382
|
+
#consume(line) {
|
|
3383
|
+
const trimmed = line.trim();
|
|
3384
|
+
if (!trimmed.includes("\"tool_execution_start\"") && !trimmed.includes("\"tool_execution_end\"")) return;
|
|
3385
|
+
let event;
|
|
3386
|
+
try {
|
|
3387
|
+
event = JSON.parse(trimmed);
|
|
3388
|
+
} catch {
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (event.type === "tool_execution_start") this.#toolOpen++;
|
|
3392
|
+
else if (event.type === "tool_execution_end") this.#toolOpen = Math.max(0, this.#toolOpen - 1);
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3344
3395
|
// ../dispatch/src/engine.ts
|
|
3345
3396
|
const DEFAULT_STOP_GRACE_MS = 5e3;
|
|
3346
3397
|
const STDOUT_KEEP_CHARS = 16777216;
|
|
@@ -3382,6 +3433,7 @@ var TaskEngine = class {
|
|
|
3382
3433
|
#terminalRecordKeep;
|
|
3383
3434
|
#archiveEnabled;
|
|
3384
3435
|
#archiveTtlDays;
|
|
3436
|
+
#stallMs;
|
|
3385
3437
|
#purgeTail = Promise.resolve();
|
|
3386
3438
|
#children = /* @__PURE__ */ new Map();
|
|
3387
3439
|
#ending = false;
|
|
@@ -3404,6 +3456,7 @@ var TaskEngine = class {
|
|
|
3404
3456
|
this.#terminalRecordKeep = options.terminalRecordKeep ?? DEFAULT_TERMINAL_RECORD_KEEP;
|
|
3405
3457
|
this.#archiveEnabled = options.archiveEnabled ?? true;
|
|
3406
3458
|
this.#archiveTtlDays = options.archiveTtlDays ?? DEFAULT_ARCHIVE_TTL_DAYS;
|
|
3459
|
+
this.#stallMs = options.stallMs ?? 3e5;
|
|
3407
3460
|
this.#ready = options.storageDir ? this.#restore(options.storageDir) : Promise.resolve();
|
|
3408
3461
|
this.#stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
|
|
3409
3462
|
const whyOpt = options.whyEnabled;
|
|
@@ -3443,7 +3496,8 @@ var TaskEngine = class {
|
|
|
3443
3496
|
label: input.label,
|
|
3444
3497
|
whySuppressed,
|
|
3445
3498
|
ownerPid: this.#owner.pid,
|
|
3446
|
-
ownerBootMs: this.#owner.bootMs
|
|
3499
|
+
ownerBootMs: this.#owner.bootMs,
|
|
3500
|
+
sessionId: this.#context.sessionId
|
|
3447
3501
|
}
|
|
3448
3502
|
}).then(async () => {
|
|
3449
3503
|
if (input.wait) {
|
|
@@ -3539,9 +3593,16 @@ var TaskEngine = class {
|
|
|
3539
3593
|
this.stop(taskId, `timeout after ${timeoutMs}ms`);
|
|
3540
3594
|
}, timeoutMs) : void 0;
|
|
3541
3595
|
timer?.unref();
|
|
3596
|
+
const activity = new ChildActivityTracker();
|
|
3597
|
+
const stallMs = this.#stallMs > 0 ? this.#stallMs : void 0;
|
|
3598
|
+
const stallGuard = stallMs ? setInterval(() => {
|
|
3599
|
+
if (activity.isStalled(Date.now(), stallMs)) this.stop(taskId, `stalled: no activity for ${stallMs}ms`);
|
|
3600
|
+
}, Math.min(STALL_CHECK_INTERVAL_MS, stallMs)) : void 0;
|
|
3601
|
+
stallGuard?.unref();
|
|
3542
3602
|
let text = "";
|
|
3543
3603
|
let outputTruncated = false;
|
|
3544
3604
|
child.onOutput((chunk) => {
|
|
3605
|
+
activity.push(chunk);
|
|
3545
3606
|
text += chunk;
|
|
3546
3607
|
if (text.length > STDOUT_KEEP_CHARS) {
|
|
3547
3608
|
text = text.slice(-16777216);
|
|
@@ -3552,6 +3613,7 @@ var TaskEngine = class {
|
|
|
3552
3613
|
child.onExit((code) => resolveExit(code));
|
|
3553
3614
|
});
|
|
3554
3615
|
if (timer) clearTimeout(timer);
|
|
3616
|
+
if (stallGuard) clearInterval(stallGuard);
|
|
3555
3617
|
this.#children.delete(taskId);
|
|
3556
3618
|
const outputPath = await this.#output.write(taskId, text);
|
|
3557
3619
|
const parsed = parseChildStream(text);
|
|
@@ -3700,6 +3762,23 @@ var TaskEngine = class {
|
|
|
3700
3762
|
return Promise.resolve(void 0);
|
|
3701
3763
|
}
|
|
3702
3764
|
}
|
|
3765
|
+
/** 归档区的全部记录(冷存按 id 存 gz)——scope=all 时一次列出;异步读,不堵宿主事件循环 */
|
|
3766
|
+
async readArchivedRecords() {
|
|
3767
|
+
const dir = this.#storageDir ? join(this.#storageDir, "archive") : null;
|
|
3768
|
+
if (!dir || !existsSync(dir)) return [];
|
|
3769
|
+
const out = [];
|
|
3770
|
+
for (const file of await readdir(dir)) {
|
|
3771
|
+
if (!file.endsWith(".json.gz")) continue;
|
|
3772
|
+
try {
|
|
3773
|
+
const parsed = JSON.parse(gunzipSync(await readFile(join(dir, file))).toString("utf8"));
|
|
3774
|
+
if (parsed && typeof parsed.id === "string") out.push({
|
|
3775
|
+
...parsed,
|
|
3776
|
+
archived: true
|
|
3777
|
+
});
|
|
3778
|
+
} catch {}
|
|
3779
|
+
}
|
|
3780
|
+
return out;
|
|
3781
|
+
}
|
|
3703
3782
|
#readArchivedOutput(taskId) {
|
|
3704
3783
|
const file = this.#storageDir ? join(this.#storageDir, "archive", `${taskId}.output.gz`) : null;
|
|
3705
3784
|
if (!file || !existsSync(file)) return void 0;
|
|
@@ -3826,152 +3905,547 @@ var TaskEngine = class {
|
|
|
3826
3905
|
await this.#purgeTail.catch(() => void 0);
|
|
3827
3906
|
}
|
|
3828
3907
|
};
|
|
3829
|
-
// ../
|
|
3830
|
-
const
|
|
3831
|
-
|
|
3832
|
-
|
|
3908
|
+
// ../tui/src/bounds.ts
|
|
3909
|
+
const RENDER_BOUNDS = {
|
|
3910
|
+
MAX_LOG_LINES_EXPANDED: 12,
|
|
3911
|
+
VALUE_TREE_MAX_DEPTH: 4,
|
|
3912
|
+
VALUE_TREE_MAX_CHILDREN: 6,
|
|
3913
|
+
VALUE_TREE_MAX_LINE_CHARS: 120
|
|
3833
3914
|
};
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
if (status === 402) return "账号余额不足:充值后再试";
|
|
3839
|
-
if (status === 408 || status === 429 || status >= 500) return "临时性失败:稍后重试";
|
|
3840
|
-
if (status >= 400) return "请求不合法:修改请求后再试(原样重试无益)";
|
|
3841
|
-
if (status === 0) return "网络不可达:检查连接后重试";
|
|
3842
|
-
return "";
|
|
3915
|
+
// ../tui/src/text.ts
|
|
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, "");
|
|
3843
3919
|
}
|
|
3844
|
-
function
|
|
3845
|
-
return
|
|
3920
|
+
function sanitizeText(text) {
|
|
3921
|
+
return stripAnsi(text.replace(/\r/g, ""));
|
|
3846
3922
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
error: `network: ${error instanceof Error ? error.message : String(error)}`
|
|
3864
|
-
};
|
|
3923
|
+
const MAX_LINE_CHARS = RENDER_BOUNDS.VALUE_TREE_MAX_LINE_CHARS;
|
|
3924
|
+
function truncateLine(line, maxChars = MAX_LINE_CHARS) {
|
|
3925
|
+
if (line.length <= maxChars) return line;
|
|
3926
|
+
let cut = maxChars;
|
|
3927
|
+
while (cut > 0 && (line.charCodeAt(cut) & 64512) === 56320) cut--;
|
|
3928
|
+
return `${line.slice(0, cut)}…`;
|
|
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);
|
|
3865
3939
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
const
|
|
3869
|
-
const
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
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;
|
|
3875
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
|
+
}
|
|
3969
|
+
// ../tui/src/truncate.ts
|
|
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;
|
|
3976
|
+
let names;
|
|
3876
3977
|
try {
|
|
3877
|
-
|
|
3878
|
-
ok: true,
|
|
3879
|
-
value: JSON.parse(text)
|
|
3880
|
-
};
|
|
3978
|
+
names = readdirSync(tmpdir());
|
|
3881
3979
|
} catch {
|
|
3882
|
-
return
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
for (const name of names) {
|
|
3983
|
+
if (!name.startsWith(TMP_FILE_PREFIX)) continue;
|
|
3984
|
+
try {
|
|
3985
|
+
const file = join(tmpdir(), name);
|
|
3986
|
+
if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS) rmSync(file, { force: true });
|
|
3987
|
+
} catch {}
|
|
3887
3988
|
}
|
|
3888
3989
|
}
|
|
3889
|
-
function
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3990
|
+
function applyAdr0015Truncation(text) {
|
|
3991
|
+
const lines = text.split("\n");
|
|
3992
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
3993
|
+
if (bytes <= 51200 && lines.length <= 2e3) return {
|
|
3994
|
+
text,
|
|
3995
|
+
truncated: false
|
|
3996
|
+
};
|
|
3997
|
+
let keptLines = lines.slice(-2e3);
|
|
3998
|
+
let kept = keptLines.join("\n");
|
|
3999
|
+
while (Buffer.byteLength(kept, "utf8") > 51200 && keptLines.length > 1) {
|
|
4000
|
+
keptLines = keptLines.slice(Math.max(1, Math.floor(keptLines.length / 2)));
|
|
4001
|
+
kept = keptLines.join("\n");
|
|
4002
|
+
}
|
|
4003
|
+
const keptBuffer = Buffer.from(kept, "utf8");
|
|
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`);
|
|
4006
|
+
mkdirSync(tmpdir(), { recursive: true });
|
|
4007
|
+
writeFileSync(fullOutputPath, text, "utf8");
|
|
4008
|
+
if (!sweptTmp) sweepExpiredTmp();
|
|
3905
4009
|
return {
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
query,
|
|
3910
|
-
count: options?.count ?? 10
|
|
3911
|
-
}, apiKey);
|
|
3912
|
-
if (!outcome.ok) return outcome;
|
|
3913
|
-
return {
|
|
3914
|
-
ok: true,
|
|
3915
|
-
value: asMatches(outcome.value)
|
|
3916
|
-
};
|
|
3917
|
-
},
|
|
3918
|
-
async open(url) {
|
|
3919
|
-
const outcome = await postJson("/fetch", { url }, apiKey);
|
|
3920
|
-
if (!outcome.ok) return outcome;
|
|
3921
|
-
const payload = outcome.value;
|
|
3922
|
-
return {
|
|
3923
|
-
ok: true,
|
|
3924
|
-
value: redact(String(payload.content ?? payload.text ?? payload.markdown ?? ""), apiKey)
|
|
3925
|
-
};
|
|
3926
|
-
}
|
|
4010
|
+
text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
|
|
4011
|
+
truncated: true,
|
|
4012
|
+
fullOutputPath
|
|
3927
4013
|
};
|
|
3928
4014
|
}
|
|
3929
|
-
// ../
|
|
3930
|
-
function
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4015
|
+
// ../task/src/list-tasks.ts
|
|
4016
|
+
function selectVisibleTasks(records, recentCompletedLimit = 10) {
|
|
4017
|
+
const inFlight = records.filter((record) => !isTerminal(record.status));
|
|
4018
|
+
const recentCompleted = records.filter((record) => isTerminal(record.status)).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, recentCompletedLimit);
|
|
4019
|
+
return [...inFlight, ...recentCompleted];
|
|
4020
|
+
}
|
|
4021
|
+
function taskFaceVisible(records) {
|
|
4022
|
+
return records.length > 0;
|
|
4023
|
+
}
|
|
4024
|
+
const TASK_TEXT_MAX_CHARS = 80;
|
|
4025
|
+
const SUMMARY_MAX_CHARS = 100;
|
|
4026
|
+
const LABEL_MAX_CHARS = 120;
|
|
4027
|
+
function projectTaskEntry(record, options = {}) {
|
|
3934
4028
|
return {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
},
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
inFlight = Math.max(0, inFlight - 1);
|
|
3944
|
-
if (success) used++;
|
|
3945
|
-
},
|
|
3946
|
-
remaining() {
|
|
3947
|
-
return unlimited ? Infinity : Math.max(0, maxCallsPerSession - used - inFlight);
|
|
3948
|
-
}
|
|
4029
|
+
id: record.id,
|
|
4030
|
+
status: record.status,
|
|
4031
|
+
...record.label ? { label: truncateLine(record.label, LABEL_MAX_CHARS) } : {},
|
|
4032
|
+
...record.finishedAt !== void 0 ? { finishedAt: record.finishedAt } : {},
|
|
4033
|
+
...record.task ? { task: truncateLine(record.task, TASK_TEXT_MAX_CHARS) } : {},
|
|
4034
|
+
...record.report?.summary ? { summary: truncateLine(record.report.summary, SUMMARY_MAX_CHARS) } : {},
|
|
4035
|
+
...options.archived ? { archived: true } : {},
|
|
4036
|
+
...options.withSession && record.sessionId ? { sessionId: record.sessionId } : {}
|
|
3949
4037
|
};
|
|
3950
4038
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
if (
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
return a === b;
|
|
4039
|
+
function fitTaskEntries(entries, limit) {
|
|
4040
|
+
const picked = [];
|
|
4041
|
+
let bytes = 2;
|
|
4042
|
+
for (const entry of entries) {
|
|
4043
|
+
if (picked.length >= limit) break;
|
|
4044
|
+
const size = Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
|
|
4045
|
+
if (bytes + size > 49152) break;
|
|
4046
|
+
picked.push(entry);
|
|
4047
|
+
bytes += size;
|
|
3961
4048
|
}
|
|
4049
|
+
return picked;
|
|
3962
4050
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
4051
|
+
// ../routes/src/surface.ts
|
|
4052
|
+
const FALLBACK_SURFACE_MODE = "full_ptc";
|
|
4053
|
+
// ../routes/src/registry.ts
|
|
4054
|
+
const TOOLS = {
|
|
4055
|
+
ptc_run_code: {
|
|
4056
|
+
name: "ptc_run_code",
|
|
4057
|
+
description: (s) => ["写一段 TypeScript 程序批量调用工具:tools.<name>(args)。支持顶层 await、Promise.all 并行、循环与条件;程序返回值是归约后的结果,中间产物不进上下文。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达,模型直走 codemode。" : [
|
|
4058
|
+
"什么时候写程序而不是逐个调工具,看系统提示词 <ptc> 节的路由表。",
|
|
4059
|
+
"程序内可调本会话已启用的同名工具(read / write / edit / grep / find / ls;bash 默认不可用),参数与直接调用该工具完全一致;每次调用返回 Promise,要 await,返回文本结果,失败抛异常。",
|
|
4060
|
+
"程序内还可调 task_records({ scope: \"all\" }) 取任务记录(all 含压缩归档)——要挑很多条时在程序里筛完只 return 要的几条。",
|
|
4061
|
+
"例:const files = await tools.find({ pattern: \"*.py\" }); for (const f of files) await tools.write({ path: f, content: body });"
|
|
4062
|
+
].join("")].join("")
|
|
4063
|
+
},
|
|
4064
|
+
ptc_workflow: {
|
|
4065
|
+
name: "ptc_workflow",
|
|
4066
|
+
description: (s) => ["结构化的多阶段程序:script 里可用 workflow.log / workflow.phase / workflow.parallel / workflow.pipeline。适合阶段明确的批处理。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达。" : "与 ptc_run_code 的取舍看 <ptc> 节的路由表。"].join("")
|
|
4067
|
+
},
|
|
4068
|
+
pi_task_codemode: {
|
|
4069
|
+
name: "pi_task_codemode",
|
|
4070
|
+
description: () => "建一条子代理任务,默认立刻返回 handle(taskId + label);wait:true 时阻塞到终态再返回结果。任务在任务记录里排队,由派发引擎起独立 pi 进程执行;完成后的报告与产出经 ptc_task_output 读取。"
|
|
4071
|
+
},
|
|
4072
|
+
ptc_task_list: {
|
|
4073
|
+
name: "ptc_task_list",
|
|
4074
|
+
description: () => "列出在飞的任务与最近完成的任务(默认最近 50 条已完成)。响应给精简条目(id/status/label/finishedAt/任务摘要)与候选总数——总数比返回条数大,说明还有更早的记录没列出来。scope=all 连压缩归档一起列(更全也更慢),status 与 session 可筛。"
|
|
4075
|
+
},
|
|
4076
|
+
ptc_task_output: {
|
|
4077
|
+
name: "ptc_task_output",
|
|
4078
|
+
description: () => "按 task_id 读一条任务记录:状态、报告、孩子的正文(text——那是交付物,报告只是摘要)与实测轨迹。正文超长时按截断契约给尾部与全文路径;记录里的 outputPath 指向原始事件流。"
|
|
4079
|
+
},
|
|
4080
|
+
ptc_task_stop: {
|
|
4081
|
+
name: "ptc_task_stop",
|
|
4082
|
+
description: () => "按 task_id 叫停任务:排队中的直接抽掉(canceled),在跑的转 stopping。"
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
const TASK_DOOR_PARAM_DESCRIPTIONS = {
|
|
4086
|
+
task: "孩子要干的活",
|
|
4087
|
+
agent: "已注册的 agent 名字;省略 = 通用孩子",
|
|
4088
|
+
agentScope: "agent 定义的查找范围(user / project / both)",
|
|
4089
|
+
why: "这一刀为什么切出来(调用方自己的话)——它会进孩子的提示词、多花孩子的 token;孩子从 task 里已经能读懂就别传",
|
|
4090
|
+
model: "覆盖孩子跑的模型",
|
|
4091
|
+
thinkingLevel: "覆盖孩子的思考档位——孩子的思考 token 是可调的成本项",
|
|
4092
|
+
wait: "true = 等这个任务完成再返回结果;false = 立刻返回 handle,结果走任务面/通知",
|
|
4093
|
+
label: "后台任务给人看的标签",
|
|
4094
|
+
readOnly: "strict = 孩子只有读工具(read/grep/find/ls,搜索开启时含 web_ds_search);lenient = 默认全工具。只需要读的活别给写权限",
|
|
4095
|
+
timeoutMs: "孩子的运行时长上限(毫秒)——超了按超时收尸;不传 = 不限(与不传时同)",
|
|
4096
|
+
projectContext: "孩子的项目上下文(AGENTS.md/CLAUDE.md):auto = 照常加载;none = 不加载——注意 none 只关「注入」不关「知识」:文件还在工作目录里,孩子会自己去翻(实测输入只差约 15%,不是省一半);这活不需要项目约定就传 none,要孩子遵守项目规矩就照常传(auto 也可能把项目里的写码规矩套进答案)"
|
|
4097
|
+
};
|
|
4098
|
+
const DELIVERY_CHECK_LEAD = "交付核对(引擎实测)";
|
|
4099
|
+
const DELIVERY_CHECK_NOTHING = "这孩子的结论没有依据:重派它,或明说缺口,别替它补数。";
|
|
4100
|
+
/** 父面渲染一行交付核对:工具/路径(实测)· 报错 · 正文长度 · 报告条数(自述) */
|
|
4101
|
+
function renderDeliveryCheck(input) {
|
|
4102
|
+
const d = input.delivery;
|
|
4103
|
+
if (!d) return "";
|
|
4104
|
+
const calls = Object.entries(d.tools);
|
|
4105
|
+
const totalCalls = calls.reduce((sum, [, count]) => sum + count, 0);
|
|
4106
|
+
const parts = [];
|
|
4107
|
+
if (calls.length > 0) {
|
|
4108
|
+
const tools = calls.map(([name, count]) => `${name}×${count}`).join(" ");
|
|
4109
|
+
const paths = d.paths.length > 0 ? `(${d.paths.slice(0, 5).join("、")}${d.paths.length > 5 ? ` 等 ${d.paths.length} 条` : ""})` : "";
|
|
4110
|
+
parts.push(`工具 ${tools}${paths}`);
|
|
4111
|
+
} else parts.push("没调用任何工具");
|
|
4112
|
+
parts.push(`报错 ${d.errors}`);
|
|
4113
|
+
parts.push(`正文 ${d.textChars} 字`);
|
|
4114
|
+
const findings = input.findings ?? 0;
|
|
4115
|
+
parts.push(`报告 ${findings} 条(${input.channel ?? "none"})`);
|
|
4116
|
+
return totalCalls === 0 && findings === 0 && d.textChars === 0 ? `${DELIVERY_CHECK_LEAD}:零产出——${parts.join(" · ")}。${DELIVERY_CHECK_NOTHING}` : `${DELIVERY_CHECK_LEAD}:${parts.join(" · ")}`;
|
|
4117
|
+
}
|
|
4118
|
+
const ROUTES = [
|
|
4119
|
+
{
|
|
4120
|
+
id: "create-a-task",
|
|
4121
|
+
name: "派一个子代理",
|
|
4122
|
+
when: "材料多、主题互不相干、每份都要读一堆材料时派——父一个人扛会算错(实测 92%→100%,n=3);成本 10 倍上下,一件能算完的活别派",
|
|
4123
|
+
how: {
|
|
4124
|
+
full_ptc: "tools.pi_task_ptc({ task, label? })(程序内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数",
|
|
4125
|
+
codemode_ptc: "tools.pi_task_codemode({ task, label? })(脚本内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数"
|
|
4126
|
+
},
|
|
4127
|
+
fact: "默认不等,立刻返回 handle;任务在任务记录里排队,可经任务面三工具查/停",
|
|
4128
|
+
detail: "docs/routes/create-a-task.md"
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
id: "run-one-program",
|
|
4132
|
+
name: "批量调工具的一段程序",
|
|
4133
|
+
when: "连续多次工具调用、结果要过滤/归约后再看",
|
|
4134
|
+
how: {
|
|
4135
|
+
full_ptc: "ptc_run_code({ code, description })",
|
|
4136
|
+
codemode_ptc: "不适用——写程序是 full_ptc 面的编排能力;本面用 codemode 编排、pi_task_codemode 派任务"
|
|
4137
|
+
},
|
|
4138
|
+
fact: "程序内 tools.*;bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线);只在本面(full_ptc)注册 PTC 工具",
|
|
4139
|
+
detail: "docs/routes/run-one-program.md"
|
|
4140
|
+
},
|
|
4141
|
+
{
|
|
4142
|
+
id: "run-a-phased-workflow",
|
|
4143
|
+
name: "分阶段 workflow",
|
|
4144
|
+
when: "批处理有明确阶段,要阶段性可见进度",
|
|
4145
|
+
how: {
|
|
4146
|
+
full_ptc: "ptc_workflow({ meta, script, args? })",
|
|
4147
|
+
codemode_ptc: "不适用——同 run-one-program:workflow 是 full_ptc 的编排能力"
|
|
4148
|
+
},
|
|
4149
|
+
fact: "script 内可用 workflow.log/phase/parallel/pipeline;只在本面注册",
|
|
4150
|
+
detail: "docs/routes/run-a-phased-workflow.md"
|
|
4151
|
+
},
|
|
4152
|
+
{
|
|
4153
|
+
id: "use-the-built-in-tools",
|
|
4154
|
+
name: "直接用内置工具",
|
|
4155
|
+
when: "单次调用就够,或每步都要模型判断(写操作、审批敏感)",
|
|
4156
|
+
how: {
|
|
4157
|
+
full_ptc: "程序内 tools.read/grep/find/ls/edit/write;逐个直调走 pi 自己的工具",
|
|
4158
|
+
codemode_ptc: "脚本内 tools.<内置名> 直接调"
|
|
4159
|
+
},
|
|
4160
|
+
fact: "bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线)",
|
|
4161
|
+
detail: "docs/routes/use-the-built-in-tools.md"
|
|
4162
|
+
},
|
|
4163
|
+
{
|
|
4164
|
+
id: "report-what-you-did",
|
|
4165
|
+
name: "交报告",
|
|
4166
|
+
when: "子代理收工:结论要有证据、副作用与用量要可核对",
|
|
4167
|
+
how: {
|
|
4168
|
+
full_ptc: "孩子的契约:最终消息末尾 fenced JSON(summary/findings/files_touched/usage);宿主自动解析",
|
|
4169
|
+
codemode_ptc: "同左——契约在孩子的追加提示词里,与 surface 无关"
|
|
4170
|
+
},
|
|
4171
|
+
fact: "reportChannel 恒在四值:tool/prompt-json/none/opted-out;工具通道(ptc_child_report)已是首选",
|
|
4172
|
+
detail: "docs/routes/report-what-you-did.md"
|
|
4173
|
+
},
|
|
4174
|
+
{
|
|
4175
|
+
id: "reach-the-web",
|
|
4176
|
+
name: "上网查资料",
|
|
4177
|
+
when: "需要网页搜索、抓页面、页内找内容——判断力在干活的子代理侧",
|
|
4178
|
+
how: {
|
|
4179
|
+
full_ptc: "派子代理(pi_task_ptc),孩子用 web_ds_search(search/open/find 三动作)",
|
|
4180
|
+
codemode_ptc: "同左——搜索只在子代理进程里注册,父会话没有这个工具"
|
|
4181
|
+
},
|
|
4182
|
+
fact: "webDsSearch 开关默认关(fail-closed);配额每进程计;概览走 snippet 超预算整条丢弃",
|
|
4183
|
+
detail: "docs/routes/reach-the-web.md"
|
|
4184
|
+
},
|
|
4185
|
+
{
|
|
4186
|
+
id: "manage-a-task",
|
|
4187
|
+
name: "管一个任务",
|
|
4188
|
+
when: "要知道孩子跑得怎样、读它的产出、或叫停它",
|
|
4189
|
+
how: {
|
|
4190
|
+
full_ptc: "ptc_task_list / ptc_task_output / ptc_task_stop(模型直调)——产出里的 delivery 是引擎实测轨迹",
|
|
4191
|
+
codemode_ptc: "同左(模型直调;脚本经结构化返回)——产出里的 delivery 是引擎实测轨迹"
|
|
4192
|
+
},
|
|
4193
|
+
fact: "有任务记录这三个工具才出现在模型面前(ADR-0007)",
|
|
4194
|
+
detail: "docs/routes/manage-a-task.md"
|
|
4195
|
+
}
|
|
4196
|
+
];
|
|
4197
|
+
const LONG_OUTPUT_GUIDANCE_WITH_REPORT = "产出超过截断阈值({bytes} 字节)。先看孩子的报告(归约层):{report}——响应里的 output 是截断后的尾部;报告没收录的完整原文用 read 读记录上的 outputPath。";
|
|
4198
|
+
const LONG_OUTPUT_GUIDANCE_NO_REPORT = "产出超过截断阈值({bytes} 字节),且孩子未交报告(reportChannel=none,散文可信度存疑)。响应里的 output 是截断后的尾部;完整原文用 read 读记录上的 outputPath。";
|
|
4199
|
+
function renderLongOutputGuidance(options) {
|
|
4200
|
+
if (options.report && options.reportChannel && options.reportChannel !== "none") {
|
|
4201
|
+
const findings = (options.report.findings ?? []).slice(0, 5).map((f) => `- ${f.what}${f.evidence ? `(证据:${f.evidence})` : ""}`).join("\n");
|
|
4202
|
+
return LONG_OUTPUT_GUIDANCE_WITH_REPORT.replace("{bytes}", String(options.bytes)).replace("{report}", [`summary: ${options.report.summary}`, findings].filter(Boolean).join("\n"));
|
|
4203
|
+
}
|
|
4204
|
+
return LONG_OUTPUT_GUIDANCE_NO_REPORT.replace("{bytes}", String(options.bytes));
|
|
4205
|
+
}
|
|
4206
|
+
const COMPACTED_TASK_REMINDER_LEAD = "对话刚被压缩——启动这些任务的消息已经不在上下文里了,但任务还在跑或已经跑完。用 ptc_task_list 看全部(scope=all 连压缩归档一起列),用 ptc_task_output 取产出(给的是孩子的正文,不是原始事件流)。";
|
|
4207
|
+
function renderCompactedTaskReminder(tasks) {
|
|
4208
|
+
const lines = tasks.map((t) => `- ${t.id}${t.label ? ` (${t.label})` : ""}: ${t.status}`);
|
|
4209
|
+
return [
|
|
4210
|
+
"<compacted-tasks>",
|
|
4211
|
+
COMPACTED_TASK_REMINDER_LEAD,
|
|
4212
|
+
...lines,
|
|
4213
|
+
"</compacted-tasks>"
|
|
4214
|
+
].join("\n");
|
|
4215
|
+
}
|
|
4216
|
+
function renderTaskNotifications(batch) {
|
|
4217
|
+
return [
|
|
4218
|
+
"<bg-task-notifications>",
|
|
4219
|
+
...batch.map((n) => `- task ${n.taskId}${n.label ? ` (${n.label})` : ""}: ${n.status}${n.stopReason ? ` — ${n.stopReason}` : ""}`),
|
|
4220
|
+
"</bg-task-notifications>"
|
|
4221
|
+
].join("\n");
|
|
4222
|
+
}
|
|
4223
|
+
function estimateTokens(text) {
|
|
4224
|
+
return Math.ceil(text.length / 2);
|
|
4225
|
+
}
|
|
4226
|
+
const ROUTE_INDEX_LEAD = {
|
|
4227
|
+
full_ptc: "批量调工具默认先写程序(ptc_run_code);单次调用或要逐步判断、审批敏感的才直连。派不派子代理自己判断——路由表第一行写了代价。",
|
|
4228
|
+
codemode_ptc: "编排写 codemode 脚本;批量派活经脚本内的 pi_task_codemode;要查/停任务用任务面三工具。派不派自己判断——路由表第一行写了代价。"
|
|
4229
|
+
};
|
|
4230
|
+
function renderRouteIndex(surface, detailBase, nudge = true) {
|
|
4231
|
+
const lines = ROUTES.map((route) => `- ${route.name} — ${route.when}。这面怎么走:${route.how[surface]}。细节:${detailBase ? route.detail.split("/").pop() : route.detail}`);
|
|
4232
|
+
let index = [
|
|
4233
|
+
`<ptc>`,
|
|
4234
|
+
...nudge ? [ROUTE_INDEX_LEAD[surface]] : [],
|
|
4235
|
+
...detailBase ? [`详情目录:${detailBase}`] : [],
|
|
4236
|
+
"路由表(一条一行,详情按需 read):",
|
|
4237
|
+
...lines,
|
|
4238
|
+
`</ptc>`
|
|
4239
|
+
].join("\n");
|
|
4240
|
+
const tokens = estimateTokens(index);
|
|
4241
|
+
if (tokens > 560) index += `\n> WARNING: index over token budget (${tokens} > 560); move detail into docs/routes/*.md`;
|
|
4242
|
+
return index;
|
|
4243
|
+
}
|
|
4244
|
+
function taskListSnapshot(registry, query = {}, archived = []) {
|
|
4245
|
+
const scope = query.scope === "all" ? "all" : "recent";
|
|
4246
|
+
const limit = Math.min(Math.max(query.limit ?? 50, 1), 200);
|
|
4247
|
+
const candidates = (scope === "all" ? [...registry.list(), ...archived] : registry.list()).filter((record) => {
|
|
4248
|
+
if (query.status && record.status !== query.status) return false;
|
|
4249
|
+
if (query.session && record.sessionId !== query.session) return false;
|
|
4250
|
+
return true;
|
|
4251
|
+
});
|
|
4252
|
+
const visible = selectVisibleTasks(candidates, limit);
|
|
4253
|
+
const withSession = query.session !== void 0;
|
|
4254
|
+
return {
|
|
4255
|
+
scope,
|
|
4256
|
+
total: candidates.length,
|
|
4257
|
+
tasks: fitTaskEntries(visible.map((record) => projectTaskEntry(record, {
|
|
4258
|
+
archived: "archived" in record,
|
|
4259
|
+
withSession
|
|
4260
|
+
})), limit)
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
function faceVisible(registry) {
|
|
4264
|
+
return taskFaceVisible(registry.list());
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* 压缩后重注入的清单:在飞任务 + 最近完成的 5 条。
|
|
4268
|
+
* 没有任务就不注入(空清单只是噪音)。
|
|
4269
|
+
*/
|
|
4270
|
+
function compactedTaskReminder(registry) {
|
|
4271
|
+
const records = registry.list();
|
|
4272
|
+
const inFlight = records.filter((record) => !isTerminal(record.status));
|
|
4273
|
+
const recentCompleted = records.filter((record) => isTerminal(record.status)).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, 5);
|
|
4274
|
+
if (inFlight.length === 0 && recentCompleted.length === 0) return void 0;
|
|
4275
|
+
return renderCompactedTaskReminder([...inFlight, ...recentCompleted].map((record) => ({
|
|
4276
|
+
id: record.id,
|
|
4277
|
+
status: record.status,
|
|
4278
|
+
label: record.label
|
|
4279
|
+
})));
|
|
4280
|
+
}
|
|
4281
|
+
// ../task/src/output.ts
|
|
4282
|
+
/**
|
|
4283
|
+
* 任务产出读法:先给归约后的孩子正文(从事件流现算,不另存一份),
|
|
4284
|
+
* 没有正文才退回原始尾部 + 分层引导(完整原文在 outputPath)。
|
|
4285
|
+
*/
|
|
4286
|
+
async function readTaskOutput(source, record) {
|
|
4287
|
+
const taskId = record.id;
|
|
4288
|
+
const outputBytes = await source.outputSize(taskId);
|
|
4289
|
+
if (outputBytes === void 0) return {};
|
|
4290
|
+
const tail = await source.readOutputTail(taskId, 51200) ?? "";
|
|
4291
|
+
let answer = parseChildStream(tail).text;
|
|
4292
|
+
if (!answer && outputBytes > 51200) answer = parseChildStream(await source.readOutput(taskId) ?? "").text;
|
|
4293
|
+
if (answer) return { text: applyAdr0015Truncation(answer).text };
|
|
4294
|
+
return {
|
|
4295
|
+
output: applyAdr0015Truncation(tail).text,
|
|
4296
|
+
guidance: renderLongOutputGuidance({
|
|
4297
|
+
bytes: outputBytes,
|
|
4298
|
+
report: record.report,
|
|
4299
|
+
reportChannel: record.reportChannel ?? "none"
|
|
4300
|
+
})
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
// ../search/src/provider.ts
|
|
4304
|
+
const SEARCH_COUNT_RANGE = {
|
|
4305
|
+
min: 1,
|
|
4306
|
+
max: 50
|
|
4307
|
+
};
|
|
4308
|
+
const SEARCH_MEMORY_MAX_CHARS = 4e5;
|
|
4309
|
+
// ../search/src/deepseek.ts
|
|
4310
|
+
const API_BASE = "https://api.deepseek.com/v1/tools";
|
|
4311
|
+
function retryHint(status) {
|
|
4312
|
+
if (status === 402) return "账号余额不足:充值后再试";
|
|
4313
|
+
if (status === 408 || status === 429 || status >= 500) return "临时性失败:稍后重试";
|
|
4314
|
+
if (status >= 400) return "请求不合法:修改请求后再试(原样重试无益)";
|
|
4315
|
+
if (status === 0) return "网络不可达:检查连接后重试";
|
|
4316
|
+
return "";
|
|
4317
|
+
}
|
|
4318
|
+
function redact(text, apiKey) {
|
|
4319
|
+
return apiKey ? text.split(apiKey).join("[REDACTED]") : text;
|
|
4320
|
+
}
|
|
4321
|
+
async function postJson(path, body, apiKey) {
|
|
4322
|
+
let response;
|
|
4323
|
+
try {
|
|
4324
|
+
response = await fetch(`${API_BASE}${path}`, {
|
|
4325
|
+
method: "POST",
|
|
4326
|
+
headers: {
|
|
4327
|
+
Authorization: `Bearer ${apiKey}`,
|
|
4328
|
+
"Content-Type": "application/json"
|
|
4329
|
+
},
|
|
4330
|
+
body: JSON.stringify(body),
|
|
4331
|
+
signal: AbortSignal.timeout(3e4)
|
|
4332
|
+
});
|
|
4333
|
+
} catch (error) {
|
|
4334
|
+
return {
|
|
4335
|
+
ok: false,
|
|
4336
|
+
status: 0,
|
|
4337
|
+
error: `network: ${error instanceof Error ? error.message : String(error)}`
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
const text = await response.text();
|
|
4341
|
+
if (!response.ok) {
|
|
4342
|
+
const base = redact(text.slice(0, 300), apiKey);
|
|
4343
|
+
const hint = retryHint(response.status);
|
|
4344
|
+
return {
|
|
4345
|
+
ok: false,
|
|
4346
|
+
status: response.status,
|
|
4347
|
+
error: hint ? `${base}(${hint})` : base
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
try {
|
|
4351
|
+
return {
|
|
4352
|
+
ok: true,
|
|
4353
|
+
value: JSON.parse(text)
|
|
4354
|
+
};
|
|
4355
|
+
} catch {
|
|
4356
|
+
return {
|
|
4357
|
+
ok: false,
|
|
4358
|
+
status: response.status,
|
|
4359
|
+
error: `bad json: ${redact(text.slice(0, 200), apiKey)}`
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
function asMatches(payload) {
|
|
4364
|
+
const list = payload?.results ?? payload?.data ?? [];
|
|
4365
|
+
if (!Array.isArray(list)) return [];
|
|
4366
|
+
return list.map((item) => {
|
|
4367
|
+
const r = item;
|
|
4368
|
+
return {
|
|
4369
|
+
title: String(r.title ?? ""),
|
|
4370
|
+
url: String(r.url ?? r.link ?? ""),
|
|
4371
|
+
snippet: r.snippet !== void 0 ? String(r.snippet) : void 0,
|
|
4372
|
+
summary: r.summary !== void 0 ? String(r.summary) : void 0,
|
|
4373
|
+
rank: typeof r.rank === "number" ? r.rank : typeof r.rerank === "number" ? r.rerank : void 0,
|
|
4374
|
+
date: r.date !== void 0 ? String(r.date) : void 0
|
|
4375
|
+
};
|
|
4376
|
+
}).filter((m) => m.url.length > 0);
|
|
4377
|
+
}
|
|
4378
|
+
function createDeepSeekProvider(apiKey) {
|
|
4379
|
+
return {
|
|
4380
|
+
id: "deepseek",
|
|
4381
|
+
async search(query, options) {
|
|
4382
|
+
const outcome = await postJson("/search", {
|
|
4383
|
+
query,
|
|
4384
|
+
count: options?.count ?? 10
|
|
4385
|
+
}, apiKey);
|
|
4386
|
+
if (!outcome.ok) return outcome;
|
|
4387
|
+
return {
|
|
4388
|
+
ok: true,
|
|
4389
|
+
value: asMatches(outcome.value)
|
|
4390
|
+
};
|
|
4391
|
+
},
|
|
4392
|
+
async open(url) {
|
|
4393
|
+
const outcome = await postJson("/fetch", { url }, apiKey);
|
|
4394
|
+
if (!outcome.ok) return outcome;
|
|
4395
|
+
const payload = outcome.value;
|
|
4396
|
+
return {
|
|
4397
|
+
ok: true,
|
|
4398
|
+
value: redact(String(payload.content ?? payload.text ?? payload.markdown ?? ""), apiKey)
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
}
|
|
4403
|
+
// ../search/src/quota.ts
|
|
4404
|
+
function createSearchQuota(maxCallsPerSession) {
|
|
4405
|
+
let inFlight = 0;
|
|
4406
|
+
let used = 0;
|
|
4407
|
+
const unlimited = maxCallsPerSession <= 0;
|
|
4408
|
+
return {
|
|
4409
|
+
tryAcquire() {
|
|
4410
|
+
if (unlimited) return true;
|
|
4411
|
+
if (used + inFlight >= maxCallsPerSession) return false;
|
|
4412
|
+
inFlight++;
|
|
4413
|
+
return true;
|
|
4414
|
+
},
|
|
4415
|
+
release(success = true) {
|
|
4416
|
+
if (unlimited) return;
|
|
4417
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
4418
|
+
if (success) used++;
|
|
4419
|
+
},
|
|
4420
|
+
remaining() {
|
|
4421
|
+
return unlimited ? Infinity : Math.max(0, maxCallsPerSession - used - inFlight);
|
|
4422
|
+
}
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
// ../search/src/memory.ts
|
|
4426
|
+
function urlsLooselyEqual(a, b) {
|
|
4427
|
+
try {
|
|
4428
|
+
const ua = new URL(a);
|
|
4429
|
+
const ub = new URL(b);
|
|
4430
|
+
if (ua.host !== ub.host || ua.protocol !== ub.protocol) return false;
|
|
4431
|
+
if (ua.pathname.replace(/\/$/, "") !== ub.pathname.replace(/\/$/, "")) return false;
|
|
4432
|
+
return ua.search === ub.search || ua.search === "" || ub.search === "";
|
|
4433
|
+
} catch {
|
|
4434
|
+
return a === b;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
function createSearchMemory() {
|
|
4438
|
+
const entries = [];
|
|
4439
|
+
const looseTexts = /* @__PURE__ */ new Map();
|
|
4440
|
+
let looseChars = 0;
|
|
4441
|
+
let totalChars = 0;
|
|
4442
|
+
const trim = () => {
|
|
4443
|
+
while (totalChars > SEARCH_MEMORY_MAX_CHARS) {
|
|
4444
|
+
const oldest = looseTexts.keys().next();
|
|
4445
|
+
if (!oldest.done) {
|
|
4446
|
+
const len = looseTexts.get(oldest.value)?.length ?? 0;
|
|
4447
|
+
looseChars -= len;
|
|
4448
|
+
totalChars -= len;
|
|
3975
4449
|
looseTexts.delete(oldest.value);
|
|
3976
4450
|
continue;
|
|
3977
4451
|
}
|
|
@@ -4013,71 +4487,9 @@ function createSearchMemory() {
|
|
|
4013
4487
|
trim();
|
|
4014
4488
|
},
|
|
4015
4489
|
lookup(url) {
|
|
4016
|
-
for (let i = entries.length - 1; i >= 0; i--) for (const [stored, text] of entries[i].textByUrl) if (urlsLooselyEqual(stored, url)) return text;
|
|
4017
|
-
for (const [stored, text] of [...looseTexts].reverse()) if (urlsLooselyEqual(stored, url)) return text;
|
|
4018
|
-
}
|
|
4019
|
-
};
|
|
4020
|
-
}
|
|
4021
|
-
// ../tui/src/bounds.ts
|
|
4022
|
-
const RENDER_BOUNDS$1 = {
|
|
4023
|
-
MAX_LOG_LINES_EXPANDED: 12,
|
|
4024
|
-
VALUE_TREE_MAX_DEPTH: 4,
|
|
4025
|
-
VALUE_TREE_MAX_CHILDREN: 6,
|
|
4026
|
-
VALUE_TREE_MAX_LINE_CHARS: 120
|
|
4027
|
-
};
|
|
4028
|
-
// ../tui/src/text.ts
|
|
4029
|
-
const ANSI_PATTERN$1 = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
4030
|
-
function stripAnsi$1(text) {
|
|
4031
|
-
return text.replace(ANSI_PATTERN$1, "");
|
|
4032
|
-
}
|
|
4033
|
-
function sanitizeText$1(text) {
|
|
4034
|
-
return stripAnsi$1(text.replace(/\r/g, ""));
|
|
4035
|
-
}
|
|
4036
|
-
RENDER_BOUNDS$1.VALUE_TREE_MAX_LINE_CHARS;
|
|
4037
|
-
// ../tui/src/truncate.ts
|
|
4038
|
-
const TRUNCATION_MAX_BYTES$1 = 51200;
|
|
4039
|
-
const TMP_FILE_TTL_MS$1 = 864e5;
|
|
4040
|
-
const TMP_FILE_PREFIX$1 = "pi-ptc-output-";
|
|
4041
|
-
let sweptTmp$1 = false;
|
|
4042
|
-
function sweepExpiredTmp$1() {
|
|
4043
|
-
sweptTmp$1 = true;
|
|
4044
|
-
let names;
|
|
4045
|
-
try {
|
|
4046
|
-
names = readdirSync(tmpdir());
|
|
4047
|
-
} catch {
|
|
4048
|
-
return;
|
|
4049
|
-
}
|
|
4050
|
-
for (const name of names) {
|
|
4051
|
-
if (!name.startsWith(TMP_FILE_PREFIX$1)) continue;
|
|
4052
|
-
try {
|
|
4053
|
-
const file = join(tmpdir(), name);
|
|
4054
|
-
if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS$1) rmSync(file, { force: true });
|
|
4055
|
-
} catch {}
|
|
4056
|
-
}
|
|
4057
|
-
}
|
|
4058
|
-
function applyAdr0015Truncation$1(text) {
|
|
4059
|
-
const lines = text.split("\n");
|
|
4060
|
-
const bytes = Buffer.byteLength(text, "utf8");
|
|
4061
|
-
if (bytes <= 51200 && lines.length <= 2e3) return {
|
|
4062
|
-
text,
|
|
4063
|
-
truncated: false
|
|
4064
|
-
};
|
|
4065
|
-
let keptLines = lines.slice(-2e3);
|
|
4066
|
-
let kept = keptLines.join("\n");
|
|
4067
|
-
while (Buffer.byteLength(kept, "utf8") > 51200 && keptLines.length > 1) {
|
|
4068
|
-
keptLines = keptLines.slice(Math.max(1, Math.floor(keptLines.length / 2)));
|
|
4069
|
-
kept = keptLines.join("\n");
|
|
4070
|
-
}
|
|
4071
|
-
const keptBuffer = Buffer.from(kept, "utf8");
|
|
4072
|
-
if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES$1).toString("utf8");
|
|
4073
|
-
const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX$1}${randomUUID()}.txt`);
|
|
4074
|
-
mkdirSync(tmpdir(), { recursive: true });
|
|
4075
|
-
writeFileSync(fullOutputPath, text, "utf8");
|
|
4076
|
-
if (!sweptTmp$1) sweepExpiredTmp$1();
|
|
4077
|
-
return {
|
|
4078
|
-
text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
|
|
4079
|
-
truncated: true,
|
|
4080
|
-
fullOutputPath
|
|
4490
|
+
for (let i = entries.length - 1; i >= 0; i--) for (const [stored, text] of entries[i].textByUrl) if (urlsLooselyEqual(stored, url)) return text;
|
|
4491
|
+
for (const [stored, text] of [...looseTexts].reverse()) if (urlsLooselyEqual(stored, url)) return text;
|
|
4492
|
+
}
|
|
4081
4493
|
};
|
|
4082
4494
|
}
|
|
4083
4495
|
// ../search/src/run.ts
|
|
@@ -4140,14 +4552,14 @@ async function runWebSearch(action, context) {
|
|
|
4140
4552
|
};
|
|
4141
4553
|
return {
|
|
4142
4554
|
ok: true,
|
|
4143
|
-
text: sanitizeText
|
|
4555
|
+
text: sanitizeText(renderSearchOverview(outcome.value, budget))
|
|
4144
4556
|
};
|
|
4145
4557
|
}
|
|
4146
4558
|
if (action.kind === "open") {
|
|
4147
4559
|
const cached = context.memory.lookup(action.url);
|
|
4148
4560
|
if (cached !== void 0) return {
|
|
4149
4561
|
ok: true,
|
|
4150
|
-
text: sanitizeText
|
|
4562
|
+
text: sanitizeText(`(记忆命中,未发请求)\n${cached.slice(0, budget)}`)
|
|
4151
4563
|
};
|
|
4152
4564
|
if (!context.quota.tryAcquire()) return {
|
|
4153
4565
|
ok: false,
|
|
@@ -4166,7 +4578,7 @@ async function runWebSearch(action, context) {
|
|
|
4166
4578
|
context.memory.storeText(action.url, outcome.value);
|
|
4167
4579
|
return {
|
|
4168
4580
|
ok: true,
|
|
4169
|
-
text: sanitizeText
|
|
4581
|
+
text: sanitizeText(outcome.value.slice(0, budget))
|
|
4170
4582
|
};
|
|
4171
4583
|
}
|
|
4172
4584
|
let regex;
|
|
@@ -4186,7 +4598,7 @@ async function runWebSearch(action, context) {
|
|
|
4186
4598
|
})).filter(({ line }) => regex.test(line));
|
|
4187
4599
|
return {
|
|
4188
4600
|
ok: true,
|
|
4189
|
-
text: sanitizeText
|
|
4601
|
+
text: sanitizeText(lines.length > 0 ? lines.slice(0, 50).map(({ line, index }) => `${index + 1}: ${line}`).join("\n") : "(页内无匹配行)").slice(0, budget)
|
|
4190
4602
|
};
|
|
4191
4603
|
}
|
|
4192
4604
|
/** 页文获取:记忆命中免费;否则占额抓取并回存记忆 */
|
|
@@ -4245,262 +4657,37 @@ function createWebDsSearchTool(context) {
|
|
|
4245
4657
|
count: args.count
|
|
4246
4658
|
} : args.action === "open" ? {
|
|
4247
4659
|
kind: "open",
|
|
4248
|
-
url: args.url ?? ""
|
|
4249
|
-
} : {
|
|
4250
|
-
kind: "find",
|
|
4251
|
-
url: args.url ?? "",
|
|
4252
|
-
pattern: args.pattern ?? ""
|
|
4253
|
-
};
|
|
4254
|
-
if (action.kind === "search" && action.query === "") return {
|
|
4255
|
-
content: [{
|
|
4256
|
-
type: "text",
|
|
4257
|
-
text: "search 动作需要 query"
|
|
4258
|
-
}],
|
|
4259
|
-
details: void 0,
|
|
4260
|
-
isError: true
|
|
4261
|
-
};
|
|
4262
|
-
if (action.kind !== "search" && action.url === "") return {
|
|
4263
|
-
content: [{
|
|
4264
|
-
type: "text",
|
|
4265
|
-
text: `${action.kind} 动作需要 url`
|
|
4266
|
-
}],
|
|
4267
|
-
details: void 0,
|
|
4268
|
-
isError: true
|
|
4269
|
-
};
|
|
4270
|
-
const result = await runWebSearch(action, context);
|
|
4271
|
-
return {
|
|
4272
|
-
content: [{
|
|
4273
|
-
type: "text",
|
|
4274
|
-
text: result.ok ? result.text : result.error
|
|
4275
|
-
}],
|
|
4276
|
-
details: void 0,
|
|
4277
|
-
isError: !result.ok
|
|
4278
|
-
};
|
|
4279
|
-
}
|
|
4280
|
-
};
|
|
4281
|
-
}
|
|
4282
|
-
// ../routes/src/surface.ts
|
|
4283
|
-
const FALLBACK_SURFACE_MODE = "full_ptc";
|
|
4284
|
-
// ../routes/src/registry.ts
|
|
4285
|
-
const TOOLS = {
|
|
4286
|
-
ptc_run_code: {
|
|
4287
|
-
name: "ptc_run_code",
|
|
4288
|
-
description: (s) => ["写一段 TypeScript 程序批量调用工具:tools.<name>(args)。支持顶层 await、Promise.all 并行、循环与条件;程序返回值是归约后的结果,中间产物不进上下文。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达,模型直走 codemode。" : [
|
|
4289
|
-
"什么时候写程序而不是逐个调工具,看系统提示词 <ptc> 节的路由表。",
|
|
4290
|
-
"程序内可调本会话已启用的同名工具(read / write / edit / grep / find / ls;bash 默认不可用),参数与直接调用该工具完全一致;每次调用返回 Promise,要 await,返回文本结果,失败抛异常。",
|
|
4291
|
-
"例:const files = await tools.find({ pattern: \"*.py\" }); for (const f of files) await tools.write({ path: f, content: body });"
|
|
4292
|
-
].join("")].join("")
|
|
4293
|
-
},
|
|
4294
|
-
ptc_workflow: {
|
|
4295
|
-
name: "ptc_workflow",
|
|
4296
|
-
description: (s) => ["结构化的多阶段程序:script 里可用 workflow.log / workflow.phase / workflow.parallel / workflow.pipeline。适合阶段明确的批处理。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达。" : "与 ptc_run_code 的取舍看 <ptc> 节的路由表。"].join("")
|
|
4297
|
-
},
|
|
4298
|
-
pi_task_codemode: {
|
|
4299
|
-
name: "pi_task_codemode",
|
|
4300
|
-
description: () => "建一条子代理任务,默认立刻返回 handle(taskId + label);wait:true 时阻塞到终态再返回结果。任务在任务记录里排队,由派发引擎起独立 pi 进程执行;完成后的报告与产出经 ptc_task_output 读取。"
|
|
4301
|
-
},
|
|
4302
|
-
ptc_task_list: {
|
|
4303
|
-
name: "ptc_task_list",
|
|
4304
|
-
description: () => "列出在飞的任务与最近完成的任务(最近 10 条已完成)。"
|
|
4305
|
-
},
|
|
4306
|
-
ptc_task_output: {
|
|
4307
|
-
name: "ptc_task_output",
|
|
4308
|
-
description: () => "按 task_id 读一条任务记录:状态、报告、产出。产出超过截断阈值时给尾部与完整产出位置(outputPath)。"
|
|
4309
|
-
},
|
|
4310
|
-
ptc_task_stop: {
|
|
4311
|
-
name: "ptc_task_stop",
|
|
4312
|
-
description: () => "按 task_id 叫停任务:排队中的直接抽掉(canceled),在跑的转 stopping。"
|
|
4313
|
-
}
|
|
4314
|
-
};
|
|
4315
|
-
const TASK_DOOR_PARAM_DESCRIPTIONS = {
|
|
4316
|
-
task: "孩子要干的活",
|
|
4317
|
-
agent: "已注册的 agent 名字;省略 = 通用孩子",
|
|
4318
|
-
agentScope: "agent 定义的查找范围(user / project / both)",
|
|
4319
|
-
why: "这一刀为什么切出来(调用方自己的话)——它会进孩子的提示词、多花孩子的 token;孩子从 task 里已经能读懂就别传",
|
|
4320
|
-
model: "覆盖孩子跑的模型",
|
|
4321
|
-
thinkingLevel: "覆盖孩子的思考档位——孩子的思考 token 是可调的成本项",
|
|
4322
|
-
wait: "true = 等这个任务完成再返回结果;false = 立刻返回 handle,结果走任务面/通知",
|
|
4323
|
-
label: "后台任务给人看的标签",
|
|
4324
|
-
readOnly: "strict = 孩子只有读工具(read/grep/find/ls,搜索开启时含 web_ds_search);lenient = 默认全工具。只需要读的活别给写权限",
|
|
4325
|
-
timeoutMs: "孩子的运行时长上限(毫秒)——超了按超时收尸;不传 = 不限(与不传时同)",
|
|
4326
|
-
projectContext: "孩子的项目上下文(AGENTS.md/CLAUDE.md):auto = 照常加载;none = 不加载——注意 none 只关「注入」不关「知识」:文件还在工作目录里,孩子会自己去翻(实测输入只差约 15%,不是省一半);这活不需要项目约定就传 none,要孩子遵守项目规矩就照常传(auto 也可能把项目里的写码规矩套进答案)"
|
|
4327
|
-
};
|
|
4328
|
-
const DELIVERY_CHECK_LEAD = "交付核对(引擎实测)";
|
|
4329
|
-
const DELIVERY_CHECK_NOTHING = "这孩子的结论没有依据:重派它,或明说缺口,别替它补数。";
|
|
4330
|
-
/** 父面渲染一行交付核对:工具/路径(实测)· 报错 · 正文长度 · 报告条数(自述) */
|
|
4331
|
-
function renderDeliveryCheck(input) {
|
|
4332
|
-
const d = input.delivery;
|
|
4333
|
-
if (!d) return "";
|
|
4334
|
-
const calls = Object.entries(d.tools);
|
|
4335
|
-
const totalCalls = calls.reduce((sum, [, count]) => sum + count, 0);
|
|
4336
|
-
const parts = [];
|
|
4337
|
-
if (calls.length > 0) {
|
|
4338
|
-
const tools = calls.map(([name, count]) => `${name}×${count}`).join(" ");
|
|
4339
|
-
const paths = d.paths.length > 0 ? `(${d.paths.slice(0, 5).join("、")}${d.paths.length > 5 ? ` 等 ${d.paths.length} 条` : ""})` : "";
|
|
4340
|
-
parts.push(`工具 ${tools}${paths}`);
|
|
4341
|
-
} else parts.push("没调用任何工具");
|
|
4342
|
-
parts.push(`报错 ${d.errors}`);
|
|
4343
|
-
parts.push(`正文 ${d.textChars} 字`);
|
|
4344
|
-
const findings = input.findings ?? 0;
|
|
4345
|
-
parts.push(`报告 ${findings} 条(${input.channel ?? "none"})`);
|
|
4346
|
-
return totalCalls === 0 && findings === 0 && d.textChars === 0 ? `${DELIVERY_CHECK_LEAD}:零产出——${parts.join(" · ")}。${DELIVERY_CHECK_NOTHING}` : `${DELIVERY_CHECK_LEAD}:${parts.join(" · ")}`;
|
|
4347
|
-
}
|
|
4348
|
-
const ROUTES = [
|
|
4349
|
-
{
|
|
4350
|
-
id: "create-a-task",
|
|
4351
|
-
name: "派一个子代理",
|
|
4352
|
-
when: "材料多、主题互不相干、每份都要读一堆材料时派——父一个人扛会算错(实测 92%→100%,n=3);成本 10 倍上下,一件能算完的活别派",
|
|
4353
|
-
how: {
|
|
4354
|
-
full_ptc: "tools.pi_task_ptc({ task, label? })(程序内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数",
|
|
4355
|
-
codemode_ptc: "tools.pi_task_codemode({ task, label? })(脚本内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数"
|
|
4356
|
-
},
|
|
4357
|
-
fact: "默认不等,立刻返回 handle;任务在任务记录里排队,可经任务面三工具查/停",
|
|
4358
|
-
detail: "docs/routes/create-a-task.md"
|
|
4359
|
-
},
|
|
4360
|
-
{
|
|
4361
|
-
id: "run-one-program",
|
|
4362
|
-
name: "批量调工具的一段程序",
|
|
4363
|
-
when: "连续多次工具调用、结果要过滤/归约后再看",
|
|
4364
|
-
how: {
|
|
4365
|
-
full_ptc: "ptc_run_code({ code, description })",
|
|
4366
|
-
codemode_ptc: "不适用——写程序是 full_ptc 面的编排能力;本面用 codemode 编排、pi_task_codemode 派任务"
|
|
4367
|
-
},
|
|
4368
|
-
fact: "程序内 tools.*;bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线);只在本面(full_ptc)注册 PTC 工具",
|
|
4369
|
-
detail: "docs/routes/run-one-program.md"
|
|
4370
|
-
},
|
|
4371
|
-
{
|
|
4372
|
-
id: "run-a-phased-workflow",
|
|
4373
|
-
name: "分阶段 workflow",
|
|
4374
|
-
when: "批处理有明确阶段,要阶段性可见进度",
|
|
4375
|
-
how: {
|
|
4376
|
-
full_ptc: "ptc_workflow({ meta, script, args? })",
|
|
4377
|
-
codemode_ptc: "不适用——同 run-one-program:workflow 是 full_ptc 的编排能力"
|
|
4378
|
-
},
|
|
4379
|
-
fact: "script 内可用 workflow.log/phase/parallel/pipeline;只在本面注册",
|
|
4380
|
-
detail: "docs/routes/run-a-phased-workflow.md"
|
|
4381
|
-
},
|
|
4382
|
-
{
|
|
4383
|
-
id: "use-the-built-in-tools",
|
|
4384
|
-
name: "直接用内置工具",
|
|
4385
|
-
when: "单次调用就够,或每步都要模型判断(写操作、审批敏感)",
|
|
4386
|
-
how: {
|
|
4387
|
-
full_ptc: "程序内 tools.read/grep/find/ls/edit/write;逐个直调走 pi 自己的工具",
|
|
4388
|
-
codemode_ptc: "脚本内 tools.<内置名> 直接调"
|
|
4389
|
-
},
|
|
4390
|
-
fact: "bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线)",
|
|
4391
|
-
detail: "docs/routes/use-the-built-in-tools.md"
|
|
4392
|
-
},
|
|
4393
|
-
{
|
|
4394
|
-
id: "report-what-you-did",
|
|
4395
|
-
name: "交报告",
|
|
4396
|
-
when: "子代理收工:结论要有证据、副作用与用量要可核对",
|
|
4397
|
-
how: {
|
|
4398
|
-
full_ptc: "孩子的契约:最终消息末尾 fenced JSON(summary/findings/files_touched/usage);宿主自动解析",
|
|
4399
|
-
codemode_ptc: "同左——契约在孩子的追加提示词里,与 surface 无关"
|
|
4400
|
-
},
|
|
4401
|
-
fact: "reportChannel 恒在四值:tool/prompt-json/none/opted-out;工具通道(ptc_child_report)已是首选",
|
|
4402
|
-
detail: "docs/routes/report-what-you-did.md"
|
|
4403
|
-
},
|
|
4404
|
-
{
|
|
4405
|
-
id: "reach-the-web",
|
|
4406
|
-
name: "上网查资料",
|
|
4407
|
-
when: "需要网页搜索、抓页面、页内找内容——判断力在干活的子代理侧",
|
|
4408
|
-
how: {
|
|
4409
|
-
full_ptc: "派子代理(pi_task_ptc),孩子用 web_ds_search(search/open/find 三动作)",
|
|
4410
|
-
codemode_ptc: "同左——搜索只在子代理进程里注册,父会话没有这个工具"
|
|
4411
|
-
},
|
|
4412
|
-
fact: "webDsSearch 开关默认关(fail-closed);配额每进程计;概览走 snippet 超预算整条丢弃",
|
|
4413
|
-
detail: "docs/routes/reach-the-web.md"
|
|
4414
|
-
},
|
|
4415
|
-
{
|
|
4416
|
-
id: "manage-a-task",
|
|
4417
|
-
name: "管一个任务",
|
|
4418
|
-
when: "要知道孩子跑得怎样、读它的产出、或叫停它",
|
|
4419
|
-
how: {
|
|
4420
|
-
full_ptc: "ptc_task_list / ptc_task_output / ptc_task_stop(模型直调)——产出里的 delivery 是引擎实测轨迹",
|
|
4421
|
-
codemode_ptc: "同左(模型直调;脚本经结构化返回)——产出里的 delivery 是引擎实测轨迹"
|
|
4422
|
-
},
|
|
4423
|
-
fact: "有任务记录这三个工具才出现在模型面前(ADR-0007)",
|
|
4424
|
-
detail: "docs/routes/manage-a-task.md"
|
|
4425
|
-
}
|
|
4426
|
-
];
|
|
4427
|
-
const LONG_OUTPUT_GUIDANCE_WITH_REPORT = "产出超过截断阈值({bytes} 字节)。先看孩子的报告(归约层):{report}——响应里的 output 是截断后的尾部;报告没收录的完整原文用 read 读记录上的 outputPath。";
|
|
4428
|
-
const LONG_OUTPUT_GUIDANCE_NO_REPORT = "产出超过截断阈值({bytes} 字节),且孩子未交报告(reportChannel=none,散文可信度存疑)。响应里的 output 是截断后的尾部;完整原文用 read 读记录上的 outputPath。";
|
|
4429
|
-
function renderLongOutputGuidance(options) {
|
|
4430
|
-
if (options.report && options.reportChannel && options.reportChannel !== "none") {
|
|
4431
|
-
const findings = (options.report.findings ?? []).slice(0, 5).map((f) => `- ${f.what}${f.evidence ? `(证据:${f.evidence})` : ""}`).join("\n");
|
|
4432
|
-
return LONG_OUTPUT_GUIDANCE_WITH_REPORT.replace("{bytes}", String(options.bytes)).replace("{report}", [`summary: ${options.report.summary}`, findings].filter(Boolean).join("\n"));
|
|
4433
|
-
}
|
|
4434
|
-
return LONG_OUTPUT_GUIDANCE_NO_REPORT.replace("{bytes}", String(options.bytes));
|
|
4435
|
-
}
|
|
4436
|
-
function renderTaskNotifications(batch) {
|
|
4437
|
-
return [
|
|
4438
|
-
"<bg-task-notifications>",
|
|
4439
|
-
...batch.map((n) => `- task ${n.taskId}${n.label ? ` (${n.label})` : ""}: ${n.status}${n.stopReason ? ` — ${n.stopReason}` : ""}`),
|
|
4440
|
-
"</bg-task-notifications>"
|
|
4441
|
-
].join("\n");
|
|
4442
|
-
}
|
|
4443
|
-
function estimateTokens(text) {
|
|
4444
|
-
return Math.ceil(text.length / 2);
|
|
4445
|
-
}
|
|
4446
|
-
const ROUTE_INDEX_LEAD = {
|
|
4447
|
-
full_ptc: "批量调工具默认先写程序(ptc_run_code);单次调用或要逐步判断、审批敏感的才直连。派不派子代理自己判断——路由表第一行写了代价。",
|
|
4448
|
-
codemode_ptc: "编排写 codemode 脚本;批量派活经脚本内的 pi_task_codemode;要查/停任务用任务面三工具。派不派自己判断——路由表第一行写了代价。"
|
|
4449
|
-
};
|
|
4450
|
-
function renderRouteIndex(surface, detailBase, nudge = true) {
|
|
4451
|
-
const lines = ROUTES.map((route) => `- ${route.name} — ${route.when}。这面怎么走:${route.how[surface]}。细节:${detailBase ? route.detail.split("/").pop() : route.detail}`);
|
|
4452
|
-
let index = [
|
|
4453
|
-
`<ptc>`,
|
|
4454
|
-
...nudge ? [ROUTE_INDEX_LEAD[surface]] : [],
|
|
4455
|
-
...detailBase ? [`详情目录:${detailBase}`] : [],
|
|
4456
|
-
"路由表(一条一行,详情按需 read):",
|
|
4457
|
-
...lines,
|
|
4458
|
-
`</ptc>`
|
|
4459
|
-
].join("\n");
|
|
4460
|
-
const tokens = estimateTokens(index);
|
|
4461
|
-
if (tokens > 560) index += `\n> WARNING: index over token budget (${tokens} > 560); move detail into docs/routes/*.md`;
|
|
4462
|
-
return index;
|
|
4463
|
-
}
|
|
4464
|
-
// ../ptc/src/task-binding.ts
|
|
4465
|
-
const TASK_BINDING_NAME = "pi_task_ptc";
|
|
4466
|
-
const TASK_DOOR_PARAMETERS = _Object_({
|
|
4467
|
-
task: String$1({
|
|
4468
|
-
minLength: 1,
|
|
4469
|
-
description: TASK_DOOR_PARAM_DESCRIPTIONS.task
|
|
4470
|
-
}),
|
|
4471
|
-
agent: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.agent })),
|
|
4472
|
-
why: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.why })),
|
|
4473
|
-
agentScope: Optional(Union([
|
|
4474
|
-
Literal("user"),
|
|
4475
|
-
Literal("project"),
|
|
4476
|
-
Literal("both")
|
|
4477
|
-
], { description: TASK_DOOR_PARAM_DESCRIPTIONS.agentScope })),
|
|
4478
|
-
model: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.model })),
|
|
4479
|
-
thinkingLevel: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.thinkingLevel })),
|
|
4480
|
-
wait: Optional(Boolean$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.wait })),
|
|
4481
|
-
label: Optional(String$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.label })),
|
|
4482
|
-
readOnly: Optional(Union([Literal("strict"), Literal("lenient")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.readOnly })),
|
|
4483
|
-
timeoutMs: Optional(Number$1({ description: TASK_DOOR_PARAM_DESCRIPTIONS.timeoutMs })),
|
|
4484
|
-
projectContext: Optional(Union([Literal("auto"), Literal("none")], { description: TASK_DOOR_PARAM_DESCRIPTIONS.projectContext }))
|
|
4485
|
-
});
|
|
4486
|
-
function createTaskBinding(door) {
|
|
4487
|
-
return {
|
|
4488
|
-
name: TASK_BINDING_NAME,
|
|
4489
|
-
parameters: TASK_DOOR_PARAMETERS,
|
|
4490
|
-
execute(args) {
|
|
4491
|
-
const input = args ?? {};
|
|
4492
|
-
return door.createTask(input.task, {
|
|
4493
|
-
label: input.label,
|
|
4494
|
-
wait: input.wait,
|
|
4495
|
-
why: input.why,
|
|
4496
|
-
model: input.model,
|
|
4497
|
-
thinkingLevel: input.thinkingLevel,
|
|
4498
|
-
agent: input.agent,
|
|
4499
|
-
agentScope: input.agentScope,
|
|
4500
|
-
readOnly: input.readOnly,
|
|
4501
|
-
timeoutMs: input.timeoutMs,
|
|
4502
|
-
projectContext: input.projectContext
|
|
4503
|
-
});
|
|
4660
|
+
url: args.url ?? ""
|
|
4661
|
+
} : {
|
|
4662
|
+
kind: "find",
|
|
4663
|
+
url: args.url ?? "",
|
|
4664
|
+
pattern: args.pattern ?? ""
|
|
4665
|
+
};
|
|
4666
|
+
if (action.kind === "search" && action.query === "") return {
|
|
4667
|
+
content: [{
|
|
4668
|
+
type: "text",
|
|
4669
|
+
text: "search 动作需要 query"
|
|
4670
|
+
}],
|
|
4671
|
+
details: void 0,
|
|
4672
|
+
isError: true
|
|
4673
|
+
};
|
|
4674
|
+
if (action.kind !== "search" && action.url === "") return {
|
|
4675
|
+
content: [{
|
|
4676
|
+
type: "text",
|
|
4677
|
+
text: `${action.kind} 动作需要 url`
|
|
4678
|
+
}],
|
|
4679
|
+
details: void 0,
|
|
4680
|
+
isError: true
|
|
4681
|
+
};
|
|
4682
|
+
const result = await runWebSearch(action, context);
|
|
4683
|
+
return {
|
|
4684
|
+
content: [{
|
|
4685
|
+
type: "text",
|
|
4686
|
+
text: result.ok ? result.text : result.error
|
|
4687
|
+
}],
|
|
4688
|
+
details: void 0,
|
|
4689
|
+
isError: !result.ok
|
|
4690
|
+
};
|
|
4504
4691
|
}
|
|
4505
4692
|
};
|
|
4506
4693
|
}
|
|
@@ -4832,127 +5019,7 @@ var WorkerPool = class {
|
|
|
4832
5019
|
this.#spawned = Math.max(0, this.#spawned - 1);
|
|
4833
5020
|
}
|
|
4834
5021
|
};
|
|
4835
|
-
// ../
|
|
4836
|
-
const KNOWN_BINDING_NAMES = [
|
|
4837
|
-
"read",
|
|
4838
|
-
"bash",
|
|
4839
|
-
"grep",
|
|
4840
|
-
"find",
|
|
4841
|
-
"ls",
|
|
4842
|
-
"edit",
|
|
4843
|
-
"write"
|
|
4844
|
-
];
|
|
4845
|
-
const OPT_IN_BINDING_NAMES = ["bash"];
|
|
4846
|
-
const DEFAULT_BINDING_NAMES = KNOWN_BINDING_NAMES.filter((name) => !OPT_IN_BINDING_NAMES.includes(name));
|
|
4847
|
-
function resolveBindingNames(activeToolNames, optIns = []) {
|
|
4848
|
-
const allowed = /* @__PURE__ */ new Set([...DEFAULT_BINDING_NAMES, ...optIns]);
|
|
4849
|
-
return KNOWN_BINDING_NAMES.filter((name) => activeToolNames.includes(name) && allowed.has(name));
|
|
4850
|
-
}
|
|
4851
|
-
// ../output/dist/index.js
|
|
4852
|
-
const RENDER_BOUNDS = {
|
|
4853
|
-
MAX_LOG_LINES_EXPANDED: 12,
|
|
4854
|
-
VALUE_TREE_MAX_DEPTH: 4,
|
|
4855
|
-
VALUE_TREE_MAX_CHILDREN: 6,
|
|
4856
|
-
VALUE_TREE_MAX_LINE_CHARS: 120
|
|
4857
|
-
};
|
|
4858
|
-
const ANSI_PATTERN = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
4859
|
-
function stripAnsi(text) {
|
|
4860
|
-
return text.replace(ANSI_PATTERN, "");
|
|
4861
|
-
}
|
|
4862
|
-
function sanitizeText(text) {
|
|
4863
|
-
return stripAnsi(text.replace(/\r/g, ""));
|
|
4864
|
-
}
|
|
4865
|
-
const MAX_LINE_CHARS = RENDER_BOUNDS.VALUE_TREE_MAX_LINE_CHARS;
|
|
4866
|
-
function truncateLine(line, maxChars = MAX_LINE_CHARS) {
|
|
4867
|
-
if (line.length <= maxChars) return line;
|
|
4868
|
-
let cut = maxChars;
|
|
4869
|
-
while (cut > 0 && (line.charCodeAt(cut) & 64512) === 56320) cut--;
|
|
4870
|
-
return `${line.slice(0, cut)}…`;
|
|
4871
|
-
}
|
|
4872
|
-
function renderModelValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
4873
|
-
const pad = " ".repeat(depth);
|
|
4874
|
-
if (depth >= RENDER_BOUNDS.VALUE_TREE_MAX_DEPTH) return "…";
|
|
4875
|
-
if (typeof value === "string") return depth === 0 ? value : JSON.stringify(value);
|
|
4876
|
-
if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4877
|
-
if (undefinedValue(value)) return "(无完成值)";
|
|
4878
|
-
if (value !== null && typeof value === "object") {
|
|
4879
|
-
if (seen.has(value)) return "[Circular]";
|
|
4880
|
-
seen.add(value);
|
|
4881
|
-
}
|
|
4882
|
-
if (Array.isArray(value)) {
|
|
4883
|
-
const shown = value.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
|
|
4884
|
-
const rest = value.length - shown.length;
|
|
4885
|
-
const oneLine = `[${shown.map((v) => JSON.stringify(v)).join(", ")}]`;
|
|
4886
|
-
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}]`;
|
|
4887
|
-
seen.delete(value);
|
|
4888
|
-
return rendered;
|
|
4889
|
-
}
|
|
4890
|
-
if (value !== null && typeof value === "object") {
|
|
4891
|
-
const entries = Object.entries(value);
|
|
4892
|
-
const shown = entries.slice(0, RENDER_BOUNDS.VALUE_TREE_MAX_CHILDREN);
|
|
4893
|
-
const rest = entries.length - shown.length;
|
|
4894
|
-
const oneLine = `{ ${shown.map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
|
|
4895
|
-
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}}`;
|
|
4896
|
-
seen.delete(value);
|
|
4897
|
-
return rendered;
|
|
4898
|
-
}
|
|
4899
|
-
return String(value);
|
|
4900
|
-
}
|
|
4901
|
-
function primitive(v) {
|
|
4902
|
-
return v === null || [
|
|
4903
|
-
"string",
|
|
4904
|
-
"number",
|
|
4905
|
-
"boolean"
|
|
4906
|
-
].includes(typeof v);
|
|
4907
|
-
}
|
|
4908
|
-
function undefinedValue(value) {
|
|
4909
|
-
return typeof value === "object" && value !== null && value.$void === true;
|
|
4910
|
-
}
|
|
4911
|
-
const TRUNCATION_MAX_BYTES = 51200;
|
|
4912
|
-
const TMP_FILE_TTL_MS = 864e5;
|
|
4913
|
-
const TMP_FILE_PREFIX = "pi-ptc-output-";
|
|
4914
|
-
let sweptTmp = false;
|
|
4915
|
-
function sweepExpiredTmp() {
|
|
4916
|
-
sweptTmp = true;
|
|
4917
|
-
let names;
|
|
4918
|
-
try {
|
|
4919
|
-
names = readdirSync(tmpdir());
|
|
4920
|
-
} catch {
|
|
4921
|
-
return;
|
|
4922
|
-
}
|
|
4923
|
-
for (const name of names) {
|
|
4924
|
-
if (!name.startsWith(TMP_FILE_PREFIX)) continue;
|
|
4925
|
-
try {
|
|
4926
|
-
const file = join(tmpdir(), name);
|
|
4927
|
-
if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS) rmSync(file, { force: true });
|
|
4928
|
-
} catch {}
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
4931
|
-
function applyAdr0015Truncation(text) {
|
|
4932
|
-
const lines = text.split("\n");
|
|
4933
|
-
const bytes = Buffer.byteLength(text, "utf8");
|
|
4934
|
-
if (bytes <= 51200 && lines.length <= 2e3) return {
|
|
4935
|
-
text,
|
|
4936
|
-
truncated: false
|
|
4937
|
-
};
|
|
4938
|
-
let keptLines = lines.slice(-2e3);
|
|
4939
|
-
let kept = keptLines.join("\n");
|
|
4940
|
-
while (Buffer.byteLength(kept, "utf8") > 51200 && keptLines.length > 1) {
|
|
4941
|
-
keptLines = keptLines.slice(Math.max(1, Math.floor(keptLines.length / 2)));
|
|
4942
|
-
kept = keptLines.join("\n");
|
|
4943
|
-
}
|
|
4944
|
-
const keptBuffer = Buffer.from(kept, "utf8");
|
|
4945
|
-
if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES).toString("utf8");
|
|
4946
|
-
const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX}${randomUUID()}.txt`);
|
|
4947
|
-
mkdirSync(tmpdir(), { recursive: true });
|
|
4948
|
-
writeFileSync(fullOutputPath, text, "utf8");
|
|
4949
|
-
if (!sweptTmp) sweepExpiredTmp();
|
|
4950
|
-
return {
|
|
4951
|
-
text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
|
|
4952
|
-
truncated: true,
|
|
4953
|
-
fullOutputPath
|
|
4954
|
-
};
|
|
4955
|
-
}
|
|
5022
|
+
// ../output/src/result-text.ts
|
|
4956
5023
|
function renderToolResult(outcome) {
|
|
4957
5024
|
let body;
|
|
4958
5025
|
if (outcome.status === "fulfilled") body = (outcome.logs.length > 0 ? [...outcome.logs, renderModelValue(outcome.value)] : [renderModelValue(outcome.value)]).join("\n");
|
|
@@ -4965,7 +5032,7 @@ function renderToolResult(outcome) {
|
|
|
4965
5032
|
fullOutputPath
|
|
4966
5033
|
};
|
|
4967
5034
|
}
|
|
4968
|
-
//
|
|
5035
|
+
// src/ptc-render.ts
|
|
4969
5036
|
function renderPtcResult(surface) {
|
|
4970
5037
|
return (result, options, _theme, context) => {
|
|
4971
5038
|
const args = context.args;
|
|
@@ -4984,14 +5051,14 @@ function renderPtcResult(surface) {
|
|
|
4984
5051
|
if (options.expanded && !options.isPartial) for (const block of result.content) {
|
|
4985
5052
|
if (block.type !== "text") continue;
|
|
4986
5053
|
const lines = block.text.split("\n");
|
|
4987
|
-
const kept = lines.slice(0, RENDER_BOUNDS
|
|
5054
|
+
const kept = lines.slice(0, RENDER_BOUNDS.MAX_LOG_LINES_EXPANDED);
|
|
4988
5055
|
for (const line of kept) box.addChild(new Text(line));
|
|
4989
5056
|
if (lines.length > kept.length) box.addChild(new Text(`…(共 ${lines.length} 行,省略 ${lines.length - kept.length} 行)`));
|
|
4990
5057
|
}
|
|
4991
5058
|
return box;
|
|
4992
5059
|
};
|
|
4993
5060
|
}
|
|
4994
|
-
//
|
|
5061
|
+
// src/ptc-tools.ts
|
|
4995
5062
|
const RUN_CODE_PARAMETERS = _Object_({
|
|
4996
5063
|
code: String$1({ description: "要执行的 TypeScript 程序体,可用顶层 await 和 return" }),
|
|
4997
5064
|
description: String$1({ description: "这段程序要干什么(给人看的一行)" }),
|
|
@@ -5106,22 +5173,66 @@ function validateWorkflowArgs(args) {
|
|
|
5106
5173
|
throw new Error(`workflow args 必须是 JSON 值:${error instanceof Error ? error.message : String(error)}`);
|
|
5107
5174
|
}
|
|
5108
5175
|
}
|
|
5109
|
-
//
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
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));
|
|
5121
5191
|
}
|
|
5122
|
-
|
|
5123
|
-
|
|
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
|
+
};
|
|
5124
5234
|
}
|
|
5235
|
+
const TASK_RECORDS_BINDING_NAME = "task_records";
|
|
5125
5236
|
// src/session.ts
|
|
5126
5237
|
function assemblePtcSession(options = {}) {
|
|
5127
5238
|
const registry = new TaskRegistry(options.onTransition);
|
|
@@ -5139,14 +5250,46 @@ function assemblePtcSession(options = {}) {
|
|
|
5139
5250
|
...opts,
|
|
5140
5251
|
task
|
|
5141
5252
|
}),
|
|
5142
|
-
|
|
5253
|
+
taskListSnapshot: (query) => taskListSnapshot(registry, query),
|
|
5143
5254
|
stopTask: (taskId, reason) => engine.stop(taskId, reason),
|
|
5144
5255
|
faceVisible: () => faceVisible(registry),
|
|
5145
5256
|
shutdown: () => engine.shutdown(),
|
|
5146
5257
|
readOutput: (taskId) => engine.readOutput(taskId),
|
|
5147
5258
|
outputSize: (taskId) => engine.outputSize(taskId),
|
|
5148
5259
|
readOutputTail: (taskId, maxBytes) => engine.readOutputTail(taskId, maxBytes),
|
|
5149
|
-
readArchivedRecord: (taskId) => engine.readArchivedRecord(taskId)
|
|
5260
|
+
readArchivedRecord: (taskId) => engine.readArchivedRecord(taskId),
|
|
5261
|
+
readArchivedRecords: () => engine.readArchivedRecords(),
|
|
5262
|
+
get sessionId() {
|
|
5263
|
+
return options.context?.sessionId;
|
|
5264
|
+
}
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
/**
|
|
5268
|
+
* 引擎运行时上下文:孩子的模型默认继承父会话的当前模型。
|
|
5269
|
+
* 优先级(旧 ADR-0044):调用参数 → agent 定义 → 会话模型(缺省继承)。
|
|
5270
|
+
* sessionModel 是活 getter——派发那一刻才读,用户中途换模型也跟着换;
|
|
5271
|
+
* floor 0.86 也有 ctx.model(实测 0.86.0 的 ExtensionContext.model 同为活 getter,provider/id 同形)→ 无版本门;
|
|
5272
|
+
* 会话没有模型 → undefined → 不发 --model;扩展被卸载后 ctx 的 getter 会抛(pi 的 assertActive)→ 吞掉退回旧行为。
|
|
5273
|
+
*/
|
|
5274
|
+
function buildEngineContext(ctx, piVersion) {
|
|
5275
|
+
return {
|
|
5276
|
+
depth: 0,
|
|
5277
|
+
piVersion,
|
|
5278
|
+
get sessionModel() {
|
|
5279
|
+
try {
|
|
5280
|
+
const model = ctx.model;
|
|
5281
|
+
return model ? `${model.provider}/${model.id}` : void 0;
|
|
5282
|
+
} catch {
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
},
|
|
5286
|
+
get sessionId() {
|
|
5287
|
+
try {
|
|
5288
|
+
return ctx.sessionManager.getSessionId();
|
|
5289
|
+
} catch {
|
|
5290
|
+
return;
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5150
5293
|
};
|
|
5151
5294
|
}
|
|
5152
5295
|
// src/index.ts
|
|
@@ -5250,10 +5393,7 @@ function piPtcEntry(pi) {
|
|
|
5250
5393
|
session = assemblePtcSession({
|
|
5251
5394
|
onTransition: () => applyTaskFaceVisibility(),
|
|
5252
5395
|
whyEnabled: () => whyEnabledOverride ?? whyEnabledPersisted,
|
|
5253
|
-
context:
|
|
5254
|
-
depth: 0,
|
|
5255
|
-
piVersion
|
|
5256
|
-
},
|
|
5396
|
+
context: buildEngineContext(ctx, piVersion),
|
|
5257
5397
|
engine: {
|
|
5258
5398
|
notifications,
|
|
5259
5399
|
resolveAgent: (name, scope) => findAgent(name, agentDirs, scope),
|
|
@@ -5329,11 +5469,34 @@ function piPtcEntry(pi) {
|
|
|
5329
5469
|
};
|
|
5330
5470
|
registerTaskFace(registerTracked, session, surface);
|
|
5331
5471
|
applyTaskFaceVisibility();
|
|
5472
|
+
let compactReminderPending = false;
|
|
5473
|
+
pi.on("session_compact", () => {
|
|
5474
|
+
compactReminderPending = true;
|
|
5475
|
+
});
|
|
5476
|
+
pi.on("context", (event) => {
|
|
5477
|
+
if (!compactReminderPending) return void 0;
|
|
5478
|
+
compactReminderPending = false;
|
|
5479
|
+
const reminder = compactedTaskReminder(session.registry);
|
|
5480
|
+
if (!reminder) return void 0;
|
|
5481
|
+
return { messages: [...event.messages, {
|
|
5482
|
+
role: "user",
|
|
5483
|
+
content: [{
|
|
5484
|
+
type: "text",
|
|
5485
|
+
text: reminder
|
|
5486
|
+
}],
|
|
5487
|
+
timestamp: Date.now()
|
|
5488
|
+
}] };
|
|
5489
|
+
});
|
|
5332
5490
|
let toolContext;
|
|
5333
|
-
const
|
|
5491
|
+
const cwd = process.cwd();
|
|
5492
|
+
const getBindings = () => createBindingTable(pi, session, () => toolContext, {
|
|
5493
|
+
cwd,
|
|
5494
|
+
extCtx: ctx,
|
|
5495
|
+
piVersion
|
|
5496
|
+
});
|
|
5334
5497
|
registerPtcTools(registerTracked, {
|
|
5335
5498
|
getBindings,
|
|
5336
|
-
cwd
|
|
5499
|
+
cwd,
|
|
5337
5500
|
surface,
|
|
5338
5501
|
getPool: () => pool,
|
|
5339
5502
|
onToolContext: (ctx) => {
|
|
@@ -5465,13 +5628,52 @@ function normalizeToolValue(value) {
|
|
|
5465
5628
|
}
|
|
5466
5629
|
return value;
|
|
5467
5630
|
}
|
|
5468
|
-
|
|
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;
|
|
5469
5649
|
const active = pi.getActiveTools();
|
|
5470
5650
|
const known = new Set(KNOWN_BINDING_NAMES);
|
|
5471
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
|
+
};
|
|
5472
5660
|
const table = Object.fromEntries(names.map((name) => [name, async (args, callCtx) => {
|
|
5473
5661
|
const ctx = getToolContext();
|
|
5474
|
-
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
|
+
}
|
|
5475
5677
|
const outcome = await ctx.executeTool(name, args, { signal: callCtx.signal });
|
|
5476
5678
|
for (const block of outcome.result.content) if (block.type === "image") callCtx.emitImage(block);
|
|
5477
5679
|
if (outcome.isError) {
|
|
@@ -5482,6 +5684,11 @@ function createBindingTable(pi, session, getToolContext) {
|
|
|
5482
5684
|
}]));
|
|
5483
5685
|
const taskBinding = createTaskBinding(session);
|
|
5484
5686
|
table[TASK_BINDING_NAME] = async (args) => withDeliveryNote(await taskBinding.execute(args));
|
|
5687
|
+
table[TASK_RECORDS_BINDING_NAME] = async (args) => {
|
|
5688
|
+
const records = session.registry.list();
|
|
5689
|
+
if (args?.scope !== "all") return records;
|
|
5690
|
+
return [...records, ...await session.readArchivedRecords()];
|
|
5691
|
+
};
|
|
5485
5692
|
return table;
|
|
5486
5693
|
}
|
|
5487
5694
|
function registerPtcTools(registerTool, options, surface) {
|
|
@@ -5494,9 +5701,23 @@ function registerTaskFace(registerTool, session, surface) {
|
|
|
5494
5701
|
name: "ptc_task_list",
|
|
5495
5702
|
label: "任务列表",
|
|
5496
5703
|
description: TOOLS.ptc_task_list.description(surface),
|
|
5497
|
-
parameters: _Object_({
|
|
5704
|
+
parameters: _Object_({
|
|
5705
|
+
scope: Optional(Union([Literal("recent"), Literal("all")], { description: "recent = 只热存(默认);all = 含压缩归档(更全但更慢)" })),
|
|
5706
|
+
limit: Optional(Number$1({ description: "取多少条(默认 50,上限 200)" })),
|
|
5707
|
+
status: Optional(Union(TASK_STATUSES.map((s) => Literal(s)), { description: "只看某个状态" })),
|
|
5708
|
+
session: Optional(String$1({ description: "按会话筛:current = 本会话,或给一个会话 id" }))
|
|
5709
|
+
}),
|
|
5498
5710
|
defaultActive: false,
|
|
5499
|
-
execute: async () =>
|
|
5711
|
+
execute: async (_id, args) => {
|
|
5712
|
+
const query = {
|
|
5713
|
+
scope: args.scope,
|
|
5714
|
+
limit: args.limit,
|
|
5715
|
+
status: args.status,
|
|
5716
|
+
session: args.session === "current" ? session.sessionId : args.session
|
|
5717
|
+
};
|
|
5718
|
+
const archived = query.scope === "all" ? await session.readArchivedRecords() : [];
|
|
5719
|
+
return textResult(taskListSnapshot(session.registry, query, archived));
|
|
5720
|
+
}
|
|
5500
5721
|
});
|
|
5501
5722
|
registerTool({
|
|
5502
5723
|
name: "ptc_task_output",
|
|
@@ -5507,24 +5728,9 @@ function registerTaskFace(registerTool, session, surface) {
|
|
|
5507
5728
|
execute: async (_id, args) => {
|
|
5508
5729
|
const record = session.registry.get(args.task_id) ?? await session.readArchivedRecord(args.task_id);
|
|
5509
5730
|
if (!record) return textResult({ error: `unknown task id ${args.task_id}` });
|
|
5510
|
-
const outputBytes = await session.outputSize(args.task_id);
|
|
5511
|
-
if (outputBytes === void 0) return textResult(withDeliveryNote(record));
|
|
5512
|
-
if (outputBytes > 51200) {
|
|
5513
|
-
const { text } = applyAdr0015Truncation$1(await session.readOutputTail(args.task_id, 51200) ?? "");
|
|
5514
|
-
return textResult(withDeliveryNote({
|
|
5515
|
-
...record,
|
|
5516
|
-
output: text,
|
|
5517
|
-
guidance: renderLongOutputGuidance({
|
|
5518
|
-
bytes: outputBytes,
|
|
5519
|
-
report: record.report,
|
|
5520
|
-
reportChannel: record.reportChannel ?? "none"
|
|
5521
|
-
})
|
|
5522
|
-
}));
|
|
5523
|
-
}
|
|
5524
|
-
const { text } = applyAdr0015Truncation$1(await session.readOutputTail(args.task_id, 51200) ?? "");
|
|
5525
5731
|
return textResult(withDeliveryNote({
|
|
5526
5732
|
...record,
|
|
5527
|
-
|
|
5733
|
+
...await readTaskOutput(session, record)
|
|
5528
5734
|
}));
|
|
5529
5735
|
}
|
|
5530
5736
|
});
|
|
@@ -5569,4 +5775,4 @@ function registerTaskDoor(registerTool, session, surface) {
|
|
|
5569
5775
|
}
|
|
5570
5776
|
});
|
|
5571
5777
|
}
|
|
5572
|
-
export { assemblePtcSession, piPtcEntry as default, withDeliveryNote };
|
|
5778
|
+
export { assemblePtcSession, buildEngineContext, piPtcEntry as default, withDeliveryNote };
|