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 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
- text: [`…(已截断:省略前 ${lines.length - keptLines.length} 行 / ${bytes - Buffer.byteLength(kept, "utf8")} 字节,全文在 ${fullOutputPath})`, kept].join("\n"),
4079
- truncated: true,
4080
- fullOutputPath
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
- listTasks: (recentCompletedLimit) => listTasks(registry, recentCompletedLimit),
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 () => textResult(session.listTasks())
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
- output: text
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()`:在飞的全部 + 最近完成的(默认 10 条)
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`:**引擎从孩子的进程事件流抽的实测轨迹**,不是孩子的自述。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ptc-subagents",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "description": "Programmable tool calling (PTC) and subagent fan-out for pi",
5
5
  "keywords": [
6
6
  "pi",