pi-ptc-subagents 7.2.0 → 7.3.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/dist/index.js +546 -320
- package/docs/routes/create-a-task.md +3 -0
- package/docs/routes/manage-a-task.md +57 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, s
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, open, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
7
7
|
import { gunzipSync, gzip } from "node:zlib";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -2746,6 +2746,15 @@ const TERMINAL_STATUSES = [
|
|
|
2746
2746
|
"canceled",
|
|
2747
2747
|
"lost"
|
|
2748
2748
|
];
|
|
2749
|
+
const TASK_STATUSES = [
|
|
2750
|
+
"queued",
|
|
2751
|
+
"running",
|
|
2752
|
+
"stopping",
|
|
2753
|
+
"succeeded",
|
|
2754
|
+
"failed",
|
|
2755
|
+
"canceled",
|
|
2756
|
+
"lost"
|
|
2757
|
+
];
|
|
2749
2758
|
function isTerminal(status) {
|
|
2750
2759
|
return TERMINAL_STATUSES.includes(status);
|
|
2751
2760
|
}
|
|
@@ -3341,6 +3350,47 @@ var NotificationCenter = class {
|
|
|
3341
3350
|
function isTerminalStatus(status) {
|
|
3342
3351
|
return status === "succeeded" || status === "failed" || status === "canceled" || status === "lost";
|
|
3343
3352
|
}
|
|
3353
|
+
/** 护栏的检查间隔(秒级问一次就够——阈值本身是分钟量级) */
|
|
3354
|
+
const STALL_CHECK_INTERVAL_MS = 1e3;
|
|
3355
|
+
var ChildActivityTracker = class {
|
|
3356
|
+
#buffer = "";
|
|
3357
|
+
#lastActivityAt;
|
|
3358
|
+
/** 在飞的工具调用计数(嵌套调用靠计数,不靠集合——start/end 成对进出) */
|
|
3359
|
+
#toolOpen = 0;
|
|
3360
|
+
#now;
|
|
3361
|
+
constructor(now = Date.now) {
|
|
3362
|
+
this.#now = now;
|
|
3363
|
+
this.#lastActivityAt = now();
|
|
3364
|
+
}
|
|
3365
|
+
/** 喂一段 stdout 增量(chunk 边界不按行切 ⇒ 自己攒半行) */
|
|
3366
|
+
push(chunk) {
|
|
3367
|
+
this.#lastActivityAt = this.#now();
|
|
3368
|
+
this.#buffer += chunk;
|
|
3369
|
+
let index = this.#buffer.indexOf("\n");
|
|
3370
|
+
while (index >= 0) {
|
|
3371
|
+
this.#consume(this.#buffer.slice(0, index));
|
|
3372
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
3373
|
+
index = this.#buffer.indexOf("\n");
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
/** 两条都成立 ⇒ 孩子什么都没在做 */
|
|
3377
|
+
isStalled(now, thresholdMs) {
|
|
3378
|
+
if (this.#toolOpen > 0) return false;
|
|
3379
|
+
return now - this.#lastActivityAt >= thresholdMs;
|
|
3380
|
+
}
|
|
3381
|
+
#consume(line) {
|
|
3382
|
+
const trimmed = line.trim();
|
|
3383
|
+
if (!trimmed.includes("\"tool_execution_start\"") && !trimmed.includes("\"tool_execution_end\"")) return;
|
|
3384
|
+
let event;
|
|
3385
|
+
try {
|
|
3386
|
+
event = JSON.parse(trimmed);
|
|
3387
|
+
} catch {
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
if (event.type === "tool_execution_start") this.#toolOpen++;
|
|
3391
|
+
else if (event.type === "tool_execution_end") this.#toolOpen = Math.max(0, this.#toolOpen - 1);
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3344
3394
|
// ../dispatch/src/engine.ts
|
|
3345
3395
|
const DEFAULT_STOP_GRACE_MS = 5e3;
|
|
3346
3396
|
const STDOUT_KEEP_CHARS = 16777216;
|
|
@@ -3382,6 +3432,7 @@ var TaskEngine = class {
|
|
|
3382
3432
|
#terminalRecordKeep;
|
|
3383
3433
|
#archiveEnabled;
|
|
3384
3434
|
#archiveTtlDays;
|
|
3435
|
+
#stallMs;
|
|
3385
3436
|
#purgeTail = Promise.resolve();
|
|
3386
3437
|
#children = /* @__PURE__ */ new Map();
|
|
3387
3438
|
#ending = false;
|
|
@@ -3404,6 +3455,7 @@ var TaskEngine = class {
|
|
|
3404
3455
|
this.#terminalRecordKeep = options.terminalRecordKeep ?? DEFAULT_TERMINAL_RECORD_KEEP;
|
|
3405
3456
|
this.#archiveEnabled = options.archiveEnabled ?? true;
|
|
3406
3457
|
this.#archiveTtlDays = options.archiveTtlDays ?? DEFAULT_ARCHIVE_TTL_DAYS;
|
|
3458
|
+
this.#stallMs = options.stallMs ?? 3e5;
|
|
3407
3459
|
this.#ready = options.storageDir ? this.#restore(options.storageDir) : Promise.resolve();
|
|
3408
3460
|
this.#stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
|
|
3409
3461
|
const whyOpt = options.whyEnabled;
|
|
@@ -3443,7 +3495,8 @@ var TaskEngine = class {
|
|
|
3443
3495
|
label: input.label,
|
|
3444
3496
|
whySuppressed,
|
|
3445
3497
|
ownerPid: this.#owner.pid,
|
|
3446
|
-
ownerBootMs: this.#owner.bootMs
|
|
3498
|
+
ownerBootMs: this.#owner.bootMs,
|
|
3499
|
+
sessionId: this.#context.sessionId
|
|
3447
3500
|
}
|
|
3448
3501
|
}).then(async () => {
|
|
3449
3502
|
if (input.wait) {
|
|
@@ -3539,9 +3592,16 @@ var TaskEngine = class {
|
|
|
3539
3592
|
this.stop(taskId, `timeout after ${timeoutMs}ms`);
|
|
3540
3593
|
}, timeoutMs) : void 0;
|
|
3541
3594
|
timer?.unref();
|
|
3595
|
+
const activity = new ChildActivityTracker();
|
|
3596
|
+
const stallMs = this.#stallMs > 0 ? this.#stallMs : void 0;
|
|
3597
|
+
const stallGuard = stallMs ? setInterval(() => {
|
|
3598
|
+
if (activity.isStalled(Date.now(), stallMs)) this.stop(taskId, `stalled: no activity for ${stallMs}ms`);
|
|
3599
|
+
}, Math.min(STALL_CHECK_INTERVAL_MS, stallMs)) : void 0;
|
|
3600
|
+
stallGuard?.unref();
|
|
3542
3601
|
let text = "";
|
|
3543
3602
|
let outputTruncated = false;
|
|
3544
3603
|
child.onOutput((chunk) => {
|
|
3604
|
+
activity.push(chunk);
|
|
3545
3605
|
text += chunk;
|
|
3546
3606
|
if (text.length > STDOUT_KEEP_CHARS) {
|
|
3547
3607
|
text = text.slice(-16777216);
|
|
@@ -3552,6 +3612,7 @@ var TaskEngine = class {
|
|
|
3552
3612
|
child.onExit((code) => resolveExit(code));
|
|
3553
3613
|
});
|
|
3554
3614
|
if (timer) clearTimeout(timer);
|
|
3615
|
+
if (stallGuard) clearInterval(stallGuard);
|
|
3555
3616
|
this.#children.delete(taskId);
|
|
3556
3617
|
const outputPath = await this.#output.write(taskId, text);
|
|
3557
3618
|
const parsed = parseChildStream(text);
|
|
@@ -3700,6 +3761,23 @@ var TaskEngine = class {
|
|
|
3700
3761
|
return Promise.resolve(void 0);
|
|
3701
3762
|
}
|
|
3702
3763
|
}
|
|
3764
|
+
/** 归档区的全部记录(冷存按 id 存 gz)——scope=all 时一次列出;异步读,不堵宿主事件循环 */
|
|
3765
|
+
async readArchivedRecords() {
|
|
3766
|
+
const dir = this.#storageDir ? join(this.#storageDir, "archive") : null;
|
|
3767
|
+
if (!dir || !existsSync(dir)) return [];
|
|
3768
|
+
const out = [];
|
|
3769
|
+
for (const file of await readdir(dir)) {
|
|
3770
|
+
if (!file.endsWith(".json.gz")) continue;
|
|
3771
|
+
try {
|
|
3772
|
+
const parsed = JSON.parse(gunzipSync(await readFile(join(dir, file))).toString("utf8"));
|
|
3773
|
+
if (parsed && typeof parsed.id === "string") out.push({
|
|
3774
|
+
...parsed,
|
|
3775
|
+
archived: true
|
|
3776
|
+
});
|
|
3777
|
+
} catch {}
|
|
3778
|
+
}
|
|
3779
|
+
return out;
|
|
3780
|
+
}
|
|
3703
3781
|
#readArchivedOutput(taskId) {
|
|
3704
3782
|
const file = this.#storageDir ? join(this.#storageDir, "archive", `${taskId}.output.gz`) : null;
|
|
3705
3783
|
if (!file || !existsSync(file)) return void 0;
|
|
@@ -3826,6 +3904,362 @@ var TaskEngine = class {
|
|
|
3826
3904
|
await this.#purgeTail.catch(() => void 0);
|
|
3827
3905
|
}
|
|
3828
3906
|
};
|
|
3907
|
+
// ../tui/src/bounds.ts
|
|
3908
|
+
const RENDER_BOUNDS$1 = {
|
|
3909
|
+
MAX_LOG_LINES_EXPANDED: 12,
|
|
3910
|
+
VALUE_TREE_MAX_DEPTH: 4,
|
|
3911
|
+
VALUE_TREE_MAX_CHILDREN: 6,
|
|
3912
|
+
VALUE_TREE_MAX_LINE_CHARS: 120
|
|
3913
|
+
};
|
|
3914
|
+
// ../tui/src/text.ts
|
|
3915
|
+
const ANSI_PATTERN$1 = /\x1B\[[0-9:;<=>?]*[ -/]*[@-~]|\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
3916
|
+
function stripAnsi$1(text) {
|
|
3917
|
+
return text.replace(ANSI_PATTERN$1, "");
|
|
3918
|
+
}
|
|
3919
|
+
function sanitizeText$1(text) {
|
|
3920
|
+
return stripAnsi$1(text.replace(/\r/g, ""));
|
|
3921
|
+
}
|
|
3922
|
+
const MAX_LINE_CHARS$1 = RENDER_BOUNDS$1.VALUE_TREE_MAX_LINE_CHARS;
|
|
3923
|
+
function truncateLine$1(line, maxChars = MAX_LINE_CHARS$1) {
|
|
3924
|
+
if (line.length <= maxChars) return line;
|
|
3925
|
+
let cut = maxChars;
|
|
3926
|
+
while (cut > 0 && (line.charCodeAt(cut) & 64512) === 56320) cut--;
|
|
3927
|
+
return `${line.slice(0, cut)}…`;
|
|
3928
|
+
}
|
|
3929
|
+
// ../tui/src/truncate.ts
|
|
3930
|
+
const TRUNCATION_MAX_BYTES$1 = 51200;
|
|
3931
|
+
const TMP_FILE_TTL_MS$1 = 864e5;
|
|
3932
|
+
const TMP_FILE_PREFIX$1 = "pi-ptc-output-";
|
|
3933
|
+
let sweptTmp$1 = false;
|
|
3934
|
+
function sweepExpiredTmp$1() {
|
|
3935
|
+
sweptTmp$1 = true;
|
|
3936
|
+
let names;
|
|
3937
|
+
try {
|
|
3938
|
+
names = readdirSync(tmpdir());
|
|
3939
|
+
} catch {
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
for (const name of names) {
|
|
3943
|
+
if (!name.startsWith(TMP_FILE_PREFIX$1)) continue;
|
|
3944
|
+
try {
|
|
3945
|
+
const file = join(tmpdir(), name);
|
|
3946
|
+
if (Date.now() - statSync(file).mtimeMs > TMP_FILE_TTL_MS$1) rmSync(file, { force: true });
|
|
3947
|
+
} catch {}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
function applyAdr0015Truncation$1(text) {
|
|
3951
|
+
const lines = text.split("\n");
|
|
3952
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
3953
|
+
if (bytes <= 51200 && lines.length <= 2e3) return {
|
|
3954
|
+
text,
|
|
3955
|
+
truncated: false
|
|
3956
|
+
};
|
|
3957
|
+
let keptLines = lines.slice(-2e3);
|
|
3958
|
+
let kept = keptLines.join("\n");
|
|
3959
|
+
while (Buffer.byteLength(kept, "utf8") > 51200 && keptLines.length > 1) {
|
|
3960
|
+
keptLines = keptLines.slice(Math.max(1, Math.floor(keptLines.length / 2)));
|
|
3961
|
+
kept = keptLines.join("\n");
|
|
3962
|
+
}
|
|
3963
|
+
const keptBuffer = Buffer.from(kept, "utf8");
|
|
3964
|
+
if (keptBuffer.length > 51200) kept = keptBuffer.subarray(keptBuffer.length - TRUNCATION_MAX_BYTES$1).toString("utf8");
|
|
3965
|
+
const fullOutputPath = join(tmpdir(), `${TMP_FILE_PREFIX$1}${randomUUID()}.txt`);
|
|
3966
|
+
mkdirSync(tmpdir(), { recursive: true });
|
|
3967
|
+
writeFileSync(fullOutputPath, text, "utf8");
|
|
3968
|
+
if (!sweptTmp$1) sweepExpiredTmp$1();
|
|
3969
|
+
return {
|
|
3970
|
+
text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
|
|
3971
|
+
truncated: true,
|
|
3972
|
+
fullOutputPath
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
// ../task/src/list-tasks.ts
|
|
3976
|
+
function selectVisibleTasks(records, recentCompletedLimit = 10) {
|
|
3977
|
+
const inFlight = records.filter((record) => !isTerminal(record.status));
|
|
3978
|
+
const recentCompleted = records.filter((record) => isTerminal(record.status)).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, recentCompletedLimit);
|
|
3979
|
+
return [...inFlight, ...recentCompleted];
|
|
3980
|
+
}
|
|
3981
|
+
function taskFaceVisible(records) {
|
|
3982
|
+
return records.length > 0;
|
|
3983
|
+
}
|
|
3984
|
+
const TASK_TEXT_MAX_CHARS = 80;
|
|
3985
|
+
const SUMMARY_MAX_CHARS = 100;
|
|
3986
|
+
const LABEL_MAX_CHARS = 120;
|
|
3987
|
+
function projectTaskEntry(record, options = {}) {
|
|
3988
|
+
return {
|
|
3989
|
+
id: record.id,
|
|
3990
|
+
status: record.status,
|
|
3991
|
+
...record.label ? { label: truncateLine$1(record.label, LABEL_MAX_CHARS) } : {},
|
|
3992
|
+
...record.finishedAt !== void 0 ? { finishedAt: record.finishedAt } : {},
|
|
3993
|
+
...record.task ? { task: truncateLine$1(record.task, TASK_TEXT_MAX_CHARS) } : {},
|
|
3994
|
+
...record.report?.summary ? { summary: truncateLine$1(record.report.summary, SUMMARY_MAX_CHARS) } : {},
|
|
3995
|
+
...options.archived ? { archived: true } : {},
|
|
3996
|
+
...options.withSession && record.sessionId ? { sessionId: record.sessionId } : {}
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
function fitTaskEntries(entries, limit) {
|
|
4000
|
+
const picked = [];
|
|
4001
|
+
let bytes = 2;
|
|
4002
|
+
for (const entry of entries) {
|
|
4003
|
+
if (picked.length >= limit) break;
|
|
4004
|
+
const size = Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
|
|
4005
|
+
if (bytes + size > 49152) break;
|
|
4006
|
+
picked.push(entry);
|
|
4007
|
+
bytes += size;
|
|
4008
|
+
}
|
|
4009
|
+
return picked;
|
|
4010
|
+
}
|
|
4011
|
+
// ../routes/src/surface.ts
|
|
4012
|
+
const FALLBACK_SURFACE_MODE = "full_ptc";
|
|
4013
|
+
// ../routes/src/registry.ts
|
|
4014
|
+
const TOOLS = {
|
|
4015
|
+
ptc_run_code: {
|
|
4016
|
+
name: "ptc_run_code",
|
|
4017
|
+
description: (s) => ["写一段 TypeScript 程序批量调用工具:tools.<name>(args)。支持顶层 await、Promise.all 并行、循环与条件;程序返回值是归约后的结果,中间产物不进上下文。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达,模型直走 codemode。" : [
|
|
4018
|
+
"什么时候写程序而不是逐个调工具,看系统提示词 <ptc> 节的路由表。",
|
|
4019
|
+
"程序内可调本会话已启用的同名工具(read / write / edit / grep / find / ls;bash 默认不可用),参数与直接调用该工具完全一致;每次调用返回 Promise,要 await,返回文本结果,失败抛异常。",
|
|
4020
|
+
"程序内还可调 task_records({ scope: \"all\" }) 取任务记录(all 含压缩归档)——要挑很多条时在程序里筛完只 return 要的几条。",
|
|
4021
|
+
"例:const files = await tools.find({ pattern: \"*.py\" }); for (const f of files) await tools.write({ path: f, content: body });"
|
|
4022
|
+
].join("")].join("")
|
|
4023
|
+
},
|
|
4024
|
+
ptc_workflow: {
|
|
4025
|
+
name: "ptc_workflow",
|
|
4026
|
+
description: (s) => ["结构化的多阶段程序:script 里可用 workflow.log / workflow.phase / workflow.parallel / workflow.pipeline。适合阶段明确的批处理。", s === "codemode_ptc" ? "本会话里我从 codemode 脚本可达。" : "与 ptc_run_code 的取舍看 <ptc> 节的路由表。"].join("")
|
|
4027
|
+
},
|
|
4028
|
+
pi_task_codemode: {
|
|
4029
|
+
name: "pi_task_codemode",
|
|
4030
|
+
description: () => "建一条子代理任务,默认立刻返回 handle(taskId + label);wait:true 时阻塞到终态再返回结果。任务在任务记录里排队,由派发引擎起独立 pi 进程执行;完成后的报告与产出经 ptc_task_output 读取。"
|
|
4031
|
+
},
|
|
4032
|
+
ptc_task_list: {
|
|
4033
|
+
name: "ptc_task_list",
|
|
4034
|
+
description: () => "列出在飞的任务与最近完成的任务(默认最近 50 条已完成)。响应给精简条目(id/status/label/finishedAt/任务摘要)与候选总数——总数比返回条数大,说明还有更早的记录没列出来。scope=all 连压缩归档一起列(更全也更慢),status 与 session 可筛。"
|
|
4035
|
+
},
|
|
4036
|
+
ptc_task_output: {
|
|
4037
|
+
name: "ptc_task_output",
|
|
4038
|
+
description: () => "按 task_id 读一条任务记录:状态、报告、孩子的正文(text——那是交付物,报告只是摘要)与实测轨迹。正文超长时按截断契约给尾部与全文路径;记录里的 outputPath 指向原始事件流。"
|
|
4039
|
+
},
|
|
4040
|
+
ptc_task_stop: {
|
|
4041
|
+
name: "ptc_task_stop",
|
|
4042
|
+
description: () => "按 task_id 叫停任务:排队中的直接抽掉(canceled),在跑的转 stopping。"
|
|
4043
|
+
}
|
|
4044
|
+
};
|
|
4045
|
+
const TASK_DOOR_PARAM_DESCRIPTIONS = {
|
|
4046
|
+
task: "孩子要干的活",
|
|
4047
|
+
agent: "已注册的 agent 名字;省略 = 通用孩子",
|
|
4048
|
+
agentScope: "agent 定义的查找范围(user / project / both)",
|
|
4049
|
+
why: "这一刀为什么切出来(调用方自己的话)——它会进孩子的提示词、多花孩子的 token;孩子从 task 里已经能读懂就别传",
|
|
4050
|
+
model: "覆盖孩子跑的模型",
|
|
4051
|
+
thinkingLevel: "覆盖孩子的思考档位——孩子的思考 token 是可调的成本项",
|
|
4052
|
+
wait: "true = 等这个任务完成再返回结果;false = 立刻返回 handle,结果走任务面/通知",
|
|
4053
|
+
label: "后台任务给人看的标签",
|
|
4054
|
+
readOnly: "strict = 孩子只有读工具(read/grep/find/ls,搜索开启时含 web_ds_search);lenient = 默认全工具。只需要读的活别给写权限",
|
|
4055
|
+
timeoutMs: "孩子的运行时长上限(毫秒)——超了按超时收尸;不传 = 不限(与不传时同)",
|
|
4056
|
+
projectContext: "孩子的项目上下文(AGENTS.md/CLAUDE.md):auto = 照常加载;none = 不加载——注意 none 只关「注入」不关「知识」:文件还在工作目录里,孩子会自己去翻(实测输入只差约 15%,不是省一半);这活不需要项目约定就传 none,要孩子遵守项目规矩就照常传(auto 也可能把项目里的写码规矩套进答案)"
|
|
4057
|
+
};
|
|
4058
|
+
const DELIVERY_CHECK_LEAD = "交付核对(引擎实测)";
|
|
4059
|
+
const DELIVERY_CHECK_NOTHING = "这孩子的结论没有依据:重派它,或明说缺口,别替它补数。";
|
|
4060
|
+
/** 父面渲染一行交付核对:工具/路径(实测)· 报错 · 正文长度 · 报告条数(自述) */
|
|
4061
|
+
function renderDeliveryCheck(input) {
|
|
4062
|
+
const d = input.delivery;
|
|
4063
|
+
if (!d) return "";
|
|
4064
|
+
const calls = Object.entries(d.tools);
|
|
4065
|
+
const totalCalls = calls.reduce((sum, [, count]) => sum + count, 0);
|
|
4066
|
+
const parts = [];
|
|
4067
|
+
if (calls.length > 0) {
|
|
4068
|
+
const tools = calls.map(([name, count]) => `${name}×${count}`).join(" ");
|
|
4069
|
+
const paths = d.paths.length > 0 ? `(${d.paths.slice(0, 5).join("、")}${d.paths.length > 5 ? ` 等 ${d.paths.length} 条` : ""})` : "";
|
|
4070
|
+
parts.push(`工具 ${tools}${paths}`);
|
|
4071
|
+
} else parts.push("没调用任何工具");
|
|
4072
|
+
parts.push(`报错 ${d.errors}`);
|
|
4073
|
+
parts.push(`正文 ${d.textChars} 字`);
|
|
4074
|
+
const findings = input.findings ?? 0;
|
|
4075
|
+
parts.push(`报告 ${findings} 条(${input.channel ?? "none"})`);
|
|
4076
|
+
return totalCalls === 0 && findings === 0 && d.textChars === 0 ? `${DELIVERY_CHECK_LEAD}:零产出——${parts.join(" · ")}。${DELIVERY_CHECK_NOTHING}` : `${DELIVERY_CHECK_LEAD}:${parts.join(" · ")}`;
|
|
4077
|
+
}
|
|
4078
|
+
const ROUTES = [
|
|
4079
|
+
{
|
|
4080
|
+
id: "create-a-task",
|
|
4081
|
+
name: "派一个子代理",
|
|
4082
|
+
when: "材料多、主题互不相干、每份都要读一堆材料时派——父一个人扛会算错(实测 92%→100%,n=3);成本 10 倍上下,一件能算完的活别派",
|
|
4083
|
+
how: {
|
|
4084
|
+
full_ptc: "tools.pi_task_ptc({ task, label? })(程序内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数",
|
|
4085
|
+
codemode_ptc: "tools.pi_task_codemode({ task, label? })(脚本内)——task 是孩子的唯一规格(写死路径/命令/交付物格式);要报告就点名 child report 的 JSON 块(summary/findings),别自创报告格式;汇总前核对孩子产出(delivery),零产出就重派别补数"
|
|
4086
|
+
},
|
|
4087
|
+
fact: "默认不等,立刻返回 handle;任务在任务记录里排队,可经任务面三工具查/停",
|
|
4088
|
+
detail: "docs/routes/create-a-task.md"
|
|
4089
|
+
},
|
|
4090
|
+
{
|
|
4091
|
+
id: "run-one-program",
|
|
4092
|
+
name: "批量调工具的一段程序",
|
|
4093
|
+
when: "连续多次工具调用、结果要过滤/归约后再看",
|
|
4094
|
+
how: {
|
|
4095
|
+
full_ptc: "ptc_run_code({ code, description })",
|
|
4096
|
+
codemode_ptc: "不适用——写程序是 full_ptc 面的编排能力;本面用 codemode 编排、pi_task_codemode 派任务"
|
|
4097
|
+
},
|
|
4098
|
+
fact: "程序内 tools.*;bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线);只在本面(full_ptc)注册 PTC 工具",
|
|
4099
|
+
detail: "docs/routes/run-one-program.md"
|
|
4100
|
+
},
|
|
4101
|
+
{
|
|
4102
|
+
id: "run-a-phased-workflow",
|
|
4103
|
+
name: "分阶段 workflow",
|
|
4104
|
+
when: "批处理有明确阶段,要阶段性可见进度",
|
|
4105
|
+
how: {
|
|
4106
|
+
full_ptc: "ptc_workflow({ meta, script, args? })",
|
|
4107
|
+
codemode_ptc: "不适用——同 run-one-program:workflow 是 full_ptc 的编排能力"
|
|
4108
|
+
},
|
|
4109
|
+
fact: "script 内可用 workflow.log/phase/parallel/pipeline;只在本面注册",
|
|
4110
|
+
detail: "docs/routes/run-a-phased-workflow.md"
|
|
4111
|
+
},
|
|
4112
|
+
{
|
|
4113
|
+
id: "use-the-built-in-tools",
|
|
4114
|
+
name: "直接用内置工具",
|
|
4115
|
+
when: "单次调用就够,或每步都要模型判断(写操作、审批敏感)",
|
|
4116
|
+
how: {
|
|
4117
|
+
full_ptc: "程序内 tools.read/grep/find/ls/edit/write;逐个直调走 pi 自己的工具",
|
|
4118
|
+
codemode_ptc: "脚本内 tools.<内置名> 直接调"
|
|
4119
|
+
},
|
|
4120
|
+
fact: "bash 当前不在程序绑定表里(旧 ADR-0040 的 opt-in 尚未接线)",
|
|
4121
|
+
detail: "docs/routes/use-the-built-in-tools.md"
|
|
4122
|
+
},
|
|
4123
|
+
{
|
|
4124
|
+
id: "report-what-you-did",
|
|
4125
|
+
name: "交报告",
|
|
4126
|
+
when: "子代理收工:结论要有证据、副作用与用量要可核对",
|
|
4127
|
+
how: {
|
|
4128
|
+
full_ptc: "孩子的契约:最终消息末尾 fenced JSON(summary/findings/files_touched/usage);宿主自动解析",
|
|
4129
|
+
codemode_ptc: "同左——契约在孩子的追加提示词里,与 surface 无关"
|
|
4130
|
+
},
|
|
4131
|
+
fact: "reportChannel 恒在四值:tool/prompt-json/none/opted-out;工具通道(ptc_child_report)已是首选",
|
|
4132
|
+
detail: "docs/routes/report-what-you-did.md"
|
|
4133
|
+
},
|
|
4134
|
+
{
|
|
4135
|
+
id: "reach-the-web",
|
|
4136
|
+
name: "上网查资料",
|
|
4137
|
+
when: "需要网页搜索、抓页面、页内找内容——判断力在干活的子代理侧",
|
|
4138
|
+
how: {
|
|
4139
|
+
full_ptc: "派子代理(pi_task_ptc),孩子用 web_ds_search(search/open/find 三动作)",
|
|
4140
|
+
codemode_ptc: "同左——搜索只在子代理进程里注册,父会话没有这个工具"
|
|
4141
|
+
},
|
|
4142
|
+
fact: "webDsSearch 开关默认关(fail-closed);配额每进程计;概览走 snippet 超预算整条丢弃",
|
|
4143
|
+
detail: "docs/routes/reach-the-web.md"
|
|
4144
|
+
},
|
|
4145
|
+
{
|
|
4146
|
+
id: "manage-a-task",
|
|
4147
|
+
name: "管一个任务",
|
|
4148
|
+
when: "要知道孩子跑得怎样、读它的产出、或叫停它",
|
|
4149
|
+
how: {
|
|
4150
|
+
full_ptc: "ptc_task_list / ptc_task_output / ptc_task_stop(模型直调)——产出里的 delivery 是引擎实测轨迹",
|
|
4151
|
+
codemode_ptc: "同左(模型直调;脚本经结构化返回)——产出里的 delivery 是引擎实测轨迹"
|
|
4152
|
+
},
|
|
4153
|
+
fact: "有任务记录这三个工具才出现在模型面前(ADR-0007)",
|
|
4154
|
+
detail: "docs/routes/manage-a-task.md"
|
|
4155
|
+
}
|
|
4156
|
+
];
|
|
4157
|
+
const LONG_OUTPUT_GUIDANCE_WITH_REPORT = "产出超过截断阈值({bytes} 字节)。先看孩子的报告(归约层):{report}——响应里的 output 是截断后的尾部;报告没收录的完整原文用 read 读记录上的 outputPath。";
|
|
4158
|
+
const LONG_OUTPUT_GUIDANCE_NO_REPORT = "产出超过截断阈值({bytes} 字节),且孩子未交报告(reportChannel=none,散文可信度存疑)。响应里的 output 是截断后的尾部;完整原文用 read 读记录上的 outputPath。";
|
|
4159
|
+
function renderLongOutputGuidance(options) {
|
|
4160
|
+
if (options.report && options.reportChannel && options.reportChannel !== "none") {
|
|
4161
|
+
const findings = (options.report.findings ?? []).slice(0, 5).map((f) => `- ${f.what}${f.evidence ? `(证据:${f.evidence})` : ""}`).join("\n");
|
|
4162
|
+
return LONG_OUTPUT_GUIDANCE_WITH_REPORT.replace("{bytes}", String(options.bytes)).replace("{report}", [`summary: ${options.report.summary}`, findings].filter(Boolean).join("\n"));
|
|
4163
|
+
}
|
|
4164
|
+
return LONG_OUTPUT_GUIDANCE_NO_REPORT.replace("{bytes}", String(options.bytes));
|
|
4165
|
+
}
|
|
4166
|
+
const COMPACTED_TASK_REMINDER_LEAD = "对话刚被压缩——启动这些任务的消息已经不在上下文里了,但任务还在跑或已经跑完。用 ptc_task_list 看全部(scope=all 连压缩归档一起列),用 ptc_task_output 取产出(给的是孩子的正文,不是原始事件流)。";
|
|
4167
|
+
function renderCompactedTaskReminder(tasks) {
|
|
4168
|
+
const lines = tasks.map((t) => `- ${t.id}${t.label ? ` (${t.label})` : ""}: ${t.status}`);
|
|
4169
|
+
return [
|
|
4170
|
+
"<compacted-tasks>",
|
|
4171
|
+
COMPACTED_TASK_REMINDER_LEAD,
|
|
4172
|
+
...lines,
|
|
4173
|
+
"</compacted-tasks>"
|
|
4174
|
+
].join("\n");
|
|
4175
|
+
}
|
|
4176
|
+
function renderTaskNotifications(batch) {
|
|
4177
|
+
return [
|
|
4178
|
+
"<bg-task-notifications>",
|
|
4179
|
+
...batch.map((n) => `- task ${n.taskId}${n.label ? ` (${n.label})` : ""}: ${n.status}${n.stopReason ? ` — ${n.stopReason}` : ""}`),
|
|
4180
|
+
"</bg-task-notifications>"
|
|
4181
|
+
].join("\n");
|
|
4182
|
+
}
|
|
4183
|
+
function estimateTokens(text) {
|
|
4184
|
+
return Math.ceil(text.length / 2);
|
|
4185
|
+
}
|
|
4186
|
+
const ROUTE_INDEX_LEAD = {
|
|
4187
|
+
full_ptc: "批量调工具默认先写程序(ptc_run_code);单次调用或要逐步判断、审批敏感的才直连。派不派子代理自己判断——路由表第一行写了代价。",
|
|
4188
|
+
codemode_ptc: "编排写 codemode 脚本;批量派活经脚本内的 pi_task_codemode;要查/停任务用任务面三工具。派不派自己判断——路由表第一行写了代价。"
|
|
4189
|
+
};
|
|
4190
|
+
function renderRouteIndex(surface, detailBase, nudge = true) {
|
|
4191
|
+
const lines = ROUTES.map((route) => `- ${route.name} — ${route.when}。这面怎么走:${route.how[surface]}。细节:${detailBase ? route.detail.split("/").pop() : route.detail}`);
|
|
4192
|
+
let index = [
|
|
4193
|
+
`<ptc>`,
|
|
4194
|
+
...nudge ? [ROUTE_INDEX_LEAD[surface]] : [],
|
|
4195
|
+
...detailBase ? [`详情目录:${detailBase}`] : [],
|
|
4196
|
+
"路由表(一条一行,详情按需 read):",
|
|
4197
|
+
...lines,
|
|
4198
|
+
`</ptc>`
|
|
4199
|
+
].join("\n");
|
|
4200
|
+
const tokens = estimateTokens(index);
|
|
4201
|
+
if (tokens > 560) index += `\n> WARNING: index over token budget (${tokens} > 560); move detail into docs/routes/*.md`;
|
|
4202
|
+
return index;
|
|
4203
|
+
}
|
|
4204
|
+
function taskListSnapshot(registry, query = {}, archived = []) {
|
|
4205
|
+
const scope = query.scope === "all" ? "all" : "recent";
|
|
4206
|
+
const limit = Math.min(Math.max(query.limit ?? 50, 1), 200);
|
|
4207
|
+
const candidates = (scope === "all" ? [...registry.list(), ...archived] : registry.list()).filter((record) => {
|
|
4208
|
+
if (query.status && record.status !== query.status) return false;
|
|
4209
|
+
if (query.session && record.sessionId !== query.session) return false;
|
|
4210
|
+
return true;
|
|
4211
|
+
});
|
|
4212
|
+
const visible = selectVisibleTasks(candidates, limit);
|
|
4213
|
+
const withSession = query.session !== void 0;
|
|
4214
|
+
return {
|
|
4215
|
+
scope,
|
|
4216
|
+
total: candidates.length,
|
|
4217
|
+
tasks: fitTaskEntries(visible.map((record) => projectTaskEntry(record, {
|
|
4218
|
+
archived: "archived" in record,
|
|
4219
|
+
withSession
|
|
4220
|
+
})), limit)
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
function faceVisible(registry) {
|
|
4224
|
+
return taskFaceVisible(registry.list());
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* 压缩后重注入的清单:在飞任务 + 最近完成的 5 条。
|
|
4228
|
+
* 没有任务就不注入(空清单只是噪音)。
|
|
4229
|
+
*/
|
|
4230
|
+
function compactedTaskReminder(registry) {
|
|
4231
|
+
const records = registry.list();
|
|
4232
|
+
const inFlight = records.filter((record) => !isTerminal(record.status));
|
|
4233
|
+
const recentCompleted = records.filter((record) => isTerminal(record.status)).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, 5);
|
|
4234
|
+
if (inFlight.length === 0 && recentCompleted.length === 0) return void 0;
|
|
4235
|
+
return renderCompactedTaskReminder([...inFlight, ...recentCompleted].map((record) => ({
|
|
4236
|
+
id: record.id,
|
|
4237
|
+
status: record.status,
|
|
4238
|
+
label: record.label
|
|
4239
|
+
})));
|
|
4240
|
+
}
|
|
4241
|
+
// ../task/src/output.ts
|
|
4242
|
+
/**
|
|
4243
|
+
* 任务产出读法:先给归约后的孩子正文(从事件流现算,不另存一份),
|
|
4244
|
+
* 没有正文才退回原始尾部 + 分层引导(完整原文在 outputPath)。
|
|
4245
|
+
*/
|
|
4246
|
+
async function readTaskOutput(source, record) {
|
|
4247
|
+
const taskId = record.id;
|
|
4248
|
+
const outputBytes = await source.outputSize(taskId);
|
|
4249
|
+
if (outputBytes === void 0) return {};
|
|
4250
|
+
const tail = await source.readOutputTail(taskId, 51200) ?? "";
|
|
4251
|
+
let answer = parseChildStream(tail).text;
|
|
4252
|
+
if (!answer && outputBytes > 51200) answer = parseChildStream(await source.readOutput(taskId) ?? "").text;
|
|
4253
|
+
if (answer) return { text: applyAdr0015Truncation$1(answer).text };
|
|
4254
|
+
return {
|
|
4255
|
+
output: applyAdr0015Truncation$1(tail).text,
|
|
4256
|
+
guidance: renderLongOutputGuidance({
|
|
4257
|
+
bytes: outputBytes,
|
|
4258
|
+
report: record.report,
|
|
4259
|
+
reportChannel: record.reportChannel ?? "none"
|
|
4260
|
+
})
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
3829
4263
|
// ../search/src/provider.ts
|
|
3830
4264
|
const SEARCH_COUNT_RANGE = {
|
|
3831
4265
|
min: 1,
|
|
@@ -3982,102 +4416,40 @@ function createSearchMemory() {
|
|
|
3982
4416
|
while (entries.length > 5) {
|
|
3983
4417
|
const evicted = entries.shift();
|
|
3984
4418
|
if (!evicted) break;
|
|
3985
|
-
totalChars -= evicted.chars;
|
|
3986
|
-
}
|
|
3987
|
-
};
|
|
3988
|
-
return {
|
|
3989
|
-
remember(query, matches) {
|
|
3990
|
-
entries.push({
|
|
3991
|
-
query,
|
|
3992
|
-
matches,
|
|
3993
|
-
textByUrl: /* @__PURE__ */ new Map(),
|
|
3994
|
-
chars: query.length
|
|
3995
|
-
});
|
|
3996
|
-
trim();
|
|
3997
|
-
},
|
|
3998
|
-
storeText(url, text) {
|
|
3999
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
4000
|
-
const entry = entries[i];
|
|
4001
|
-
const match = entry.matches.find((m) => urlsLooselyEqual(m.url, url));
|
|
4002
|
-
if (!match) continue;
|
|
4003
|
-
entry.textByUrl.set(match.url, text);
|
|
4004
|
-
entry.chars += text.length;
|
|
4005
|
-
totalChars += text.length;
|
|
4006
|
-
trim();
|
|
4007
|
-
return;
|
|
4008
|
-
}
|
|
4009
|
-
looseChars -= looseTexts.get(url)?.length ?? 0;
|
|
4010
|
-
looseTexts.set(url, text);
|
|
4011
|
-
looseChars += text.length;
|
|
4012
|
-
totalChars += text.length;
|
|
4013
|
-
trim();
|
|
4014
|
-
},
|
|
4015
|
-
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();
|
|
4419
|
+
totalChars -= evicted.chars;
|
|
4420
|
+
}
|
|
4421
|
+
};
|
|
4077
4422
|
return {
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4423
|
+
remember(query, matches) {
|
|
4424
|
+
entries.push({
|
|
4425
|
+
query,
|
|
4426
|
+
matches,
|
|
4427
|
+
textByUrl: /* @__PURE__ */ new Map(),
|
|
4428
|
+
chars: query.length
|
|
4429
|
+
});
|
|
4430
|
+
trim();
|
|
4431
|
+
},
|
|
4432
|
+
storeText(url, text) {
|
|
4433
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
4434
|
+
const entry = entries[i];
|
|
4435
|
+
const match = entry.matches.find((m) => urlsLooselyEqual(m.url, url));
|
|
4436
|
+
if (!match) continue;
|
|
4437
|
+
entry.textByUrl.set(match.url, text);
|
|
4438
|
+
entry.chars += text.length;
|
|
4439
|
+
totalChars += text.length;
|
|
4440
|
+
trim();
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
looseChars -= looseTexts.get(url)?.length ?? 0;
|
|
4444
|
+
looseTexts.set(url, text);
|
|
4445
|
+
looseChars += text.length;
|
|
4446
|
+
totalChars += text.length;
|
|
4447
|
+
trim();
|
|
4448
|
+
},
|
|
4449
|
+
lookup(url) {
|
|
4450
|
+
for (let i = entries.length - 1; i >= 0; i--) for (const [stored, text] of entries[i].textByUrl) if (urlsLooselyEqual(stored, url)) return text;
|
|
4451
|
+
for (const [stored, text] of [...looseTexts].reverse()) if (urlsLooselyEqual(stored, url)) return text;
|
|
4452
|
+
}
|
|
4081
4453
|
};
|
|
4082
4454
|
}
|
|
4083
4455
|
// ../search/src/run.ts
|
|
@@ -4279,188 +4651,6 @@ function createWebDsSearchTool(context) {
|
|
|
4279
4651
|
}
|
|
4280
4652
|
};
|
|
4281
4653
|
}
|
|
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
4654
|
// ../ptc/src/task-binding.ts
|
|
4465
4655
|
const TASK_BINDING_NAME = "pi_task_ptc";
|
|
4466
4656
|
const TASK_DOOR_PARAMETERS = _Object_({
|
|
@@ -4504,6 +4694,7 @@ function createTaskBinding(door) {
|
|
|
4504
4694
|
}
|
|
4505
4695
|
};
|
|
4506
4696
|
}
|
|
4697
|
+
const TASK_RECORDS_BINDING_NAME = "task_records";
|
|
4507
4698
|
// ../ptc/src/limits.ts
|
|
4508
4699
|
const DEFAULT_LIMITS = {
|
|
4509
4700
|
timeoutMs: 12e4,
|
|
@@ -5106,22 +5297,6 @@ function validateWorkflowArgs(args) {
|
|
|
5106
5297
|
throw new Error(`workflow args 必须是 JSON 值:${error instanceof Error ? error.message : String(error)}`);
|
|
5107
5298
|
}
|
|
5108
5299
|
}
|
|
5109
|
-
// ../task/src/list-tasks.ts
|
|
5110
|
-
function selectVisibleTasks(records, recentCompletedLimit = 10) {
|
|
5111
|
-
const inFlight = records.filter((record) => !isTerminal(record.status));
|
|
5112
|
-
const recentCompleted = records.filter((record) => isTerminal(record.status)).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, recentCompletedLimit);
|
|
5113
|
-
return [...inFlight, ...recentCompleted];
|
|
5114
|
-
}
|
|
5115
|
-
function taskFaceVisible(records) {
|
|
5116
|
-
return records.length > 0;
|
|
5117
|
-
}
|
|
5118
|
-
// ../task/src/lifecycle.ts
|
|
5119
|
-
function listTasks(registry, recentCompletedLimit) {
|
|
5120
|
-
return selectVisibleTasks(registry.list(), recentCompletedLimit);
|
|
5121
|
-
}
|
|
5122
|
-
function faceVisible(registry) {
|
|
5123
|
-
return taskFaceVisible(registry.list());
|
|
5124
|
-
}
|
|
5125
5300
|
// src/session.ts
|
|
5126
5301
|
function assemblePtcSession(options = {}) {
|
|
5127
5302
|
const registry = new TaskRegistry(options.onTransition);
|
|
@@ -5139,14 +5314,46 @@ function assemblePtcSession(options = {}) {
|
|
|
5139
5314
|
...opts,
|
|
5140
5315
|
task
|
|
5141
5316
|
}),
|
|
5142
|
-
|
|
5317
|
+
taskListSnapshot: (query) => taskListSnapshot(registry, query),
|
|
5143
5318
|
stopTask: (taskId, reason) => engine.stop(taskId, reason),
|
|
5144
5319
|
faceVisible: () => faceVisible(registry),
|
|
5145
5320
|
shutdown: () => engine.shutdown(),
|
|
5146
5321
|
readOutput: (taskId) => engine.readOutput(taskId),
|
|
5147
5322
|
outputSize: (taskId) => engine.outputSize(taskId),
|
|
5148
5323
|
readOutputTail: (taskId, maxBytes) => engine.readOutputTail(taskId, maxBytes),
|
|
5149
|
-
readArchivedRecord: (taskId) => engine.readArchivedRecord(taskId)
|
|
5324
|
+
readArchivedRecord: (taskId) => engine.readArchivedRecord(taskId),
|
|
5325
|
+
readArchivedRecords: () => engine.readArchivedRecords(),
|
|
5326
|
+
get sessionId() {
|
|
5327
|
+
return options.context?.sessionId;
|
|
5328
|
+
}
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5331
|
+
/**
|
|
5332
|
+
* 引擎运行时上下文:孩子的模型默认继承父会话的当前模型。
|
|
5333
|
+
* 优先级(旧 ADR-0044):调用参数 → agent 定义 → 会话模型(缺省继承)。
|
|
5334
|
+
* sessionModel 是活 getter——派发那一刻才读,用户中途换模型也跟着换;
|
|
5335
|
+
* floor 0.86 也有 ctx.model(实测 0.86.0 的 ExtensionContext.model 同为活 getter,provider/id 同形)→ 无版本门;
|
|
5336
|
+
* 会话没有模型 → undefined → 不发 --model;扩展被卸载后 ctx 的 getter 会抛(pi 的 assertActive)→ 吞掉退回旧行为。
|
|
5337
|
+
*/
|
|
5338
|
+
function buildEngineContext(ctx, piVersion) {
|
|
5339
|
+
return {
|
|
5340
|
+
depth: 0,
|
|
5341
|
+
piVersion,
|
|
5342
|
+
get sessionModel() {
|
|
5343
|
+
try {
|
|
5344
|
+
const model = ctx.model;
|
|
5345
|
+
return model ? `${model.provider}/${model.id}` : void 0;
|
|
5346
|
+
} catch {
|
|
5347
|
+
return;
|
|
5348
|
+
}
|
|
5349
|
+
},
|
|
5350
|
+
get sessionId() {
|
|
5351
|
+
try {
|
|
5352
|
+
return ctx.sessionManager.getSessionId();
|
|
5353
|
+
} catch {
|
|
5354
|
+
return;
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5150
5357
|
};
|
|
5151
5358
|
}
|
|
5152
5359
|
// src/index.ts
|
|
@@ -5250,10 +5457,7 @@ function piPtcEntry(pi) {
|
|
|
5250
5457
|
session = assemblePtcSession({
|
|
5251
5458
|
onTransition: () => applyTaskFaceVisibility(),
|
|
5252
5459
|
whyEnabled: () => whyEnabledOverride ?? whyEnabledPersisted,
|
|
5253
|
-
context:
|
|
5254
|
-
depth: 0,
|
|
5255
|
-
piVersion
|
|
5256
|
-
},
|
|
5460
|
+
context: buildEngineContext(ctx, piVersion),
|
|
5257
5461
|
engine: {
|
|
5258
5462
|
notifications,
|
|
5259
5463
|
resolveAgent: (name, scope) => findAgent(name, agentDirs, scope),
|
|
@@ -5329,6 +5533,24 @@ function piPtcEntry(pi) {
|
|
|
5329
5533
|
};
|
|
5330
5534
|
registerTaskFace(registerTracked, session, surface);
|
|
5331
5535
|
applyTaskFaceVisibility();
|
|
5536
|
+
let compactReminderPending = false;
|
|
5537
|
+
pi.on("session_compact", () => {
|
|
5538
|
+
compactReminderPending = true;
|
|
5539
|
+
});
|
|
5540
|
+
pi.on("context", (event) => {
|
|
5541
|
+
if (!compactReminderPending) return void 0;
|
|
5542
|
+
compactReminderPending = false;
|
|
5543
|
+
const reminder = compactedTaskReminder(session.registry);
|
|
5544
|
+
if (!reminder) return void 0;
|
|
5545
|
+
return { messages: [...event.messages, {
|
|
5546
|
+
role: "user",
|
|
5547
|
+
content: [{
|
|
5548
|
+
type: "text",
|
|
5549
|
+
text: reminder
|
|
5550
|
+
}],
|
|
5551
|
+
timestamp: Date.now()
|
|
5552
|
+
}] };
|
|
5553
|
+
});
|
|
5332
5554
|
let toolContext;
|
|
5333
5555
|
const getBindings = () => createBindingTable(pi, session, () => toolContext);
|
|
5334
5556
|
registerPtcTools(registerTracked, {
|
|
@@ -5482,6 +5704,11 @@ function createBindingTable(pi, session, getToolContext) {
|
|
|
5482
5704
|
}]));
|
|
5483
5705
|
const taskBinding = createTaskBinding(session);
|
|
5484
5706
|
table[TASK_BINDING_NAME] = async (args) => withDeliveryNote(await taskBinding.execute(args));
|
|
5707
|
+
table[TASK_RECORDS_BINDING_NAME] = async (args) => {
|
|
5708
|
+
const records = session.registry.list();
|
|
5709
|
+
if (args?.scope !== "all") return records;
|
|
5710
|
+
return [...records, ...await session.readArchivedRecords()];
|
|
5711
|
+
};
|
|
5485
5712
|
return table;
|
|
5486
5713
|
}
|
|
5487
5714
|
function registerPtcTools(registerTool, options, surface) {
|
|
@@ -5494,9 +5721,23 @@ function registerTaskFace(registerTool, session, surface) {
|
|
|
5494
5721
|
name: "ptc_task_list",
|
|
5495
5722
|
label: "任务列表",
|
|
5496
5723
|
description: TOOLS.ptc_task_list.description(surface),
|
|
5497
|
-
parameters: _Object_({
|
|
5724
|
+
parameters: _Object_({
|
|
5725
|
+
scope: Optional(Union([Literal("recent"), Literal("all")], { description: "recent = 只热存(默认);all = 含压缩归档(更全但更慢)" })),
|
|
5726
|
+
limit: Optional(Number$1({ description: "取多少条(默认 50,上限 200)" })),
|
|
5727
|
+
status: Optional(Union(TASK_STATUSES.map((s) => Literal(s)), { description: "只看某个状态" })),
|
|
5728
|
+
session: Optional(String$1({ description: "按会话筛:current = 本会话,或给一个会话 id" }))
|
|
5729
|
+
}),
|
|
5498
5730
|
defaultActive: false,
|
|
5499
|
-
execute: async () =>
|
|
5731
|
+
execute: async (_id, args) => {
|
|
5732
|
+
const query = {
|
|
5733
|
+
scope: args.scope,
|
|
5734
|
+
limit: args.limit,
|
|
5735
|
+
status: args.status,
|
|
5736
|
+
session: args.session === "current" ? session.sessionId : args.session
|
|
5737
|
+
};
|
|
5738
|
+
const archived = query.scope === "all" ? await session.readArchivedRecords() : [];
|
|
5739
|
+
return textResult(taskListSnapshot(session.registry, query, archived));
|
|
5740
|
+
}
|
|
5500
5741
|
});
|
|
5501
5742
|
registerTool({
|
|
5502
5743
|
name: "ptc_task_output",
|
|
@@ -5507,24 +5748,9 @@ function registerTaskFace(registerTool, session, surface) {
|
|
|
5507
5748
|
execute: async (_id, args) => {
|
|
5508
5749
|
const record = session.registry.get(args.task_id) ?? await session.readArchivedRecord(args.task_id);
|
|
5509
5750
|
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
5751
|
return textResult(withDeliveryNote({
|
|
5526
5752
|
...record,
|
|
5527
|
-
|
|
5753
|
+
...await readTaskOutput(session, record)
|
|
5528
5754
|
}));
|
|
5529
5755
|
}
|
|
5530
5756
|
});
|
|
@@ -5569,4 +5795,4 @@ function registerTaskDoor(registerTool, session, surface) {
|
|
|
5569
5795
|
}
|
|
5570
5796
|
});
|
|
5571
5797
|
}
|
|
5572
|
-
export { assemblePtcSession, piPtcEntry as default, withDeliveryNote };
|
|
5798
|
+
export { assemblePtcSession, buildEngineContext, piPtcEntry as default, withDeliveryNote };
|
|
@@ -15,6 +15,9 @@ agent 定义里的 `tools / model / thinkingLevel / childReport` 由 frontmatter
|
|
|
15
15
|
任务建成后在任务记录里排队(满员排队不拒绝,ADR-0010),经
|
|
16
16
|
`ptc_task_list / ptc_task_output / ptc_task_stop` 查/停。
|
|
17
17
|
|
|
18
|
+
任务记录带宿主会话 id(`sessionId`)——按会话筛任务用 `ptc_task_list({ session: "current" })`,
|
|
19
|
+
跨会话的旧记录用 `scope=all` 找。压缩归档(`scope=all`)里的条目也带 `archived: true`。
|
|
20
|
+
|
|
18
21
|
## 什么时候值得派(判据)
|
|
19
22
|
|
|
20
23
|
- **值得**:主题多、彼此独立、每个都要读一堆材料——父一个人扛会算错,一份材料一个孩子最划算。
|
|
@@ -2,15 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
三个工具,有任务记录才出现在模型面前(ADR-0007:可见性 = 有无记录,不是独立开关):
|
|
4
4
|
|
|
5
|
-
- `ptc_task_list()`:在飞的全部 +
|
|
6
|
-
- `ptc_task_output({ task_id })
|
|
5
|
+
- `ptc_task_list()`:在飞的全部 + 最近完成的任务。响应是对象。
|
|
6
|
+
- `ptc_task_output({ task_id })`:读一条记录。响应是对象。
|
|
7
7
|
- `ptc_task_stop({ task_id, reason? })`:排队中的抽掉(canceled);在跑的转 stopping
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
任务记录是派发的唯一痕迹:每次建任务都建记录,等不等也一样建。
|
|
10
10
|
|
|
11
11
|
终态记录热存留最近 500 条(决策 29),被挤出的进压缩存档(默认留 30 天,
|
|
12
12
|
`archiveEnabled: false` 则直接删),凭精确 task id 经 `ptc_task_output` 仍读得到。
|
|
13
13
|
|
|
14
|
+
## ptc_task_list
|
|
15
|
+
|
|
16
|
+
参数:
|
|
17
|
+
|
|
18
|
+
- `scope`:`recent`(默认,只热存)/ `all`(连压缩归档一起列,更全也更慢)
|
|
19
|
+
- `limit`:取多少条(默认 50,上限 200)
|
|
20
|
+
- `status`:只看某个状态(七个状态之一)
|
|
21
|
+
- `session`:`current` = 本会话,或给一个会话 id
|
|
22
|
+
|
|
23
|
+
响应:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scope": "recent",
|
|
28
|
+
"total": 520,
|
|
29
|
+
"tasks": [
|
|
30
|
+
{
|
|
31
|
+
"id": "5d90e136…",
|
|
32
|
+
"status": "succeeded",
|
|
33
|
+
"label": "口径汇总",
|
|
34
|
+
"finishedAt": 1758000123456,
|
|
35
|
+
"task": "读 AGENTS.md 并汇总六个包的对外口径",
|
|
36
|
+
"summary": "六个包口径一致"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `total` = 候选总数。它比 `tasks.length` 大,说明还有更早的记录没列出来。
|
|
43
|
+
- 每条字段各自封顶(task 80 字、summary 100 字、label 120 字)。响应按字节预算逐条裁剪,超预算就停——返回的永远是合法 JSON(不会砍成半截 JSON)。
|
|
44
|
+
- `scope=all` 时归档条目带 `archived: true`。
|
|
45
|
+
- `session` 筛选时条目回显 `sessionId`。老记录没有这个字段,不匹配任何会话。
|
|
46
|
+
|
|
47
|
+
## ptc_task_output
|
|
48
|
+
|
|
49
|
+
参数:`task_id`(一条任务记录 id)。
|
|
50
|
+
|
|
51
|
+
响应 = 那条记录 + 归约层:
|
|
52
|
+
|
|
53
|
+
- `text`:孩子的散文答案。**交付物是这段正文**,报告只是摘要。正文从事件流现算,不另存一份。
|
|
54
|
+
- 没有正文时给 `output`(原始事件流尾部,50KB 截断,砍头留尾)+ `guidance`(分层引导:先看报告,完整原文在 `outputPath`)。
|
|
55
|
+
- `report`:孩子交的报告(`summary` + `findings`)。孩子没交报告时,这个键整个不存在。
|
|
56
|
+
- `outputPath`:原始事件流的路径。
|
|
57
|
+
|
|
58
|
+
registry 里记不住的 id 会回落压缩归档(TTL 窗口内仍可解,标 `archived: true`)。
|
|
59
|
+
|
|
60
|
+
## 程序内检索
|
|
61
|
+
|
|
62
|
+
`ptc_run_code` 的程序内可调 `tools.task_records({ scope: "all" })`:拿全部任务记录(含压缩归档)。
|
|
63
|
+
|
|
64
|
+
机械扫描在宿主做,筛选与归约在程序里做。程序只 `return` 要的几条,其余不进上下文。
|
|
65
|
+
|
|
66
|
+
正则跑在运行格里,运行格有 terminate 兜底;宿主本身没有隔离——病态正则要在程序里跑,别在宿主里跑。
|
|
67
|
+
|
|
14
68
|
## 交付核对(delivery)
|
|
15
69
|
|
|
16
70
|
任务记录与产出里带 `delivery`:**引擎从孩子的进程事件流抽的实测轨迹**,不是孩子的自述。
|