@te-river/opencode-team-mode 1.7.2 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1009 -849
- package/README.zh-CN.md +906 -756
- package/dist/host/v2-browser-gate.d.ts +4 -1
- package/dist/host/v2-browser-gate.d.ts.map +1 -1
- package/dist/host/v2-browser-gate.js +1 -1
- package/dist/host/v2-browser-gate.js.map +1 -1
- package/dist/host/v2-capabilities.js +1 -1
- package/dist/host/v2-capabilities.js.map +1 -1
- package/dist/host/v2-guard.d.ts +3 -1
- package/dist/host/v2-guard.d.ts.map +1 -1
- package/dist/host/v2-guard.js +2 -2
- package/dist/host/v2-guard.js.map +1 -1
- package/dist/host/v2-offload.d.ts +12 -2
- package/dist/host/v2-offload.d.ts.map +1 -1
- package/dist/host/v2-offload.js +9 -9
- package/dist/host/v2-offload.js.map +1 -1
- package/dist/host/v2-probe-chain.d.ts +5 -1
- package/dist/host/v2-probe-chain.d.ts.map +1 -1
- package/dist/host/v2-probe-chain.js +4 -6
- package/dist/host/v2-probe-chain.js.map +1 -1
- package/dist/host/v2-probe.d.ts.map +1 -1
- package/dist/host/v2-probe.js +2 -1
- package/dist/host/v2-probe.js.map +1 -1
- package/dist/host/v2-prune.d.ts +5 -1
- package/dist/host/v2-prune.d.ts.map +1 -1
- package/dist/host/v2-prune.js +8 -14
- package/dist/host/v2-prune.js.map +1 -1
- package/dist/host/v2-retry.d.ts +9 -2
- package/dist/host/v2-retry.d.ts.map +1 -1
- package/dist/host/v2-retry.js +13 -15
- package/dist/host/v2-retry.js.map +1 -1
- package/dist/host/v2-session.d.ts +6 -2
- package/dist/host/v2-session.d.ts.map +1 -1
- package/dist/host/v2-session.js +7 -13
- package/dist/host/v2-session.js.map +1 -1
- package/dist/host/v2.d.ts +7 -2
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +38 -35
- package/dist/host/v2.js.map +1 -1
- package/dist/prompts/lead.d.ts +1 -1
- package/dist/prompts/lead.d.ts.map +1 -1
- package/dist/prompts/lead.js +23 -7
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/specialists.d.ts +1 -1
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +1 -1
- package/dist/tm/args-schema.d.ts +1 -1
- package/dist/tm/args-schema.js +6 -6
- package/dist/tm/args-schema.js.map +1 -1
- package/dist/tm/board.js +2 -2
- package/dist/tm/board.js.map +1 -1
- package/dist/tm/config-files.d.ts +41 -17
- package/dist/tm/config-files.d.ts.map +1 -1
- package/dist/tm/config-files.js +68 -73
- package/dist/tm/config-files.js.map +1 -1
- package/dist/tm/config-layers.d.ts +65 -33
- package/dist/tm/config-layers.d.ts.map +1 -1
- package/dist/tm/config-layers.js +165 -81
- package/dist/tm/config-layers.js.map +1 -1
- package/dist/tm/config-template.d.ts +32 -0
- package/dist/tm/config-template.d.ts.map +1 -0
- package/dist/tm/config-template.js +74 -0
- package/dist/tm/config-template.js.map +1 -0
- package/dist/tm/config.d.ts +115 -140
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +100 -200
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/index.d.ts +11 -3
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +46 -16
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +7 -3
- package/dist/tm/ledger.d.ts.map +1 -1
- package/dist/tm/ledger.js +5 -4
- package/dist/tm/ledger.js.map +1 -1
- package/dist/tm/memory.js +3 -3
- package/dist/tm/pipelines.js +1 -1
- package/dist/tm/search.js +6 -6
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/stats.js +2 -2
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/tools.js +2 -2
- package/dist/tm/webfetch.d.ts +5 -2
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +24 -18
- package/dist/tm/webfetch.js.map +1 -1
- package/docs/installation-v2.md +59 -38
- package/docs/installation.md +4 -4
- package/package.json +1 -1
- package/dist/approval-gate.d.ts +0 -205
- package/dist/approval-gate.d.ts.map +0 -1
- package/dist/approval-gate.js +0 -627
- package/dist/approval-gate.js.map +0 -1
- package/dist/host/v1.d.ts +0 -14
- package/dist/host/v1.d.ts.map +0 -1
- package/dist/host/v1.js +0 -340
- package/dist/host/v1.js.map +0 -1
- package/dist/host/v2-client.d.ts +0 -53
- package/dist/host/v2-client.d.ts.map +0 -1
- package/dist/host/v2-client.js +0 -193
- package/dist/host/v2-client.js.map +0 -1
- package/dist/host/v2-transcript.d.ts +0 -73
- package/dist/host/v2-transcript.d.ts.map +0 -1
- package/dist/host/v2-transcript.js +0 -151
- package/dist/host/v2-transcript.js.map +0 -1
- package/dist/host-hooks.d.ts +0 -126
- package/dist/host-hooks.d.ts.map +0 -1
- package/dist/host-hooks.js +0 -284
- package/dist/host-hooks.js.map +0 -1
- package/dist/tm/bash-timeout.d.ts +0 -72
- package/dist/tm/bash-timeout.d.ts.map +0 -1
- package/dist/tm/bash-timeout.js +0 -100
- package/dist/tm/bash-timeout.js.map +0 -1
- package/dist/tm/browser.d.ts +0 -642
- package/dist/tm/browser.d.ts.map +0 -1
- package/dist/tm/browser.js +0 -2836
- package/dist/tm/browser.js.map +0 -1
- package/dist/tm/ptc/budgets.d.ts +0 -59
- package/dist/tm/ptc/budgets.d.ts.map +0 -1
- package/dist/tm/ptc/budgets.js +0 -138
- package/dist/tm/ptc/budgets.js.map +0 -1
- package/dist/tm/ptc/contract.d.ts +0 -171
- package/dist/tm/ptc/contract.d.ts.map +0 -1
- package/dist/tm/ptc/contract.js +0 -85
- package/dist/tm/ptc/contract.js.map +0 -1
- package/dist/tm/ptc/driver.d.ts +0 -40
- package/dist/tm/ptc/driver.d.ts.map +0 -1
- package/dist/tm/ptc/driver.js +0 -213
- package/dist/tm/ptc/driver.js.map +0 -1
- package/dist/tm/ptc/engines.d.ts +0 -43
- package/dist/tm/ptc/engines.d.ts.map +0 -1
- package/dist/tm/ptc/engines.js +0 -454
- package/dist/tm/ptc/engines.js.map +0 -1
- package/dist/tm/ptc/gate.d.ts +0 -28
- package/dist/tm/ptc/gate.d.ts.map +0 -1
- package/dist/tm/ptc/gate.js +0 -130
- package/dist/tm/ptc/gate.js.map +0 -1
- package/dist/tm/ptc/index.d.ts +0 -19
- package/dist/tm/ptc/index.d.ts.map +0 -1
- package/dist/tm/ptc/index.js +0 -15
- package/dist/tm/ptc/index.js.map +0 -1
- package/dist/tm/ptc/pscan.d.ts +0 -30
- package/dist/tm/ptc/pscan.d.ts.map +0 -1
- package/dist/tm/ptc/pscan.js +0 -341
- package/dist/tm/ptc/pscan.js.map +0 -1
- package/dist/tm/ptc/summary.d.ts +0 -17
- package/dist/tm/ptc/summary.d.ts.map +0 -1
- package/dist/tm/ptc/summary.js +0 -134
- package/dist/tm/ptc/summary.js.map +0 -1
- package/dist/tm/ptc/tool.d.ts +0 -66
- package/dist/tm/ptc/tool.d.ts.map +0 -1
- package/dist/tm/ptc/tool.js +0 -231
- package/dist/tm/ptc/tool.js.map +0 -1
- package/dist/tm/ptc.d.ts +0 -298
- package/dist/tm/ptc.d.ts.map +0 -1
- package/dist/tm/ptc.js +0 -865
- package/dist/tm/ptc.js.map +0 -1
- package/dist/tm/pty.d.ts +0 -70
- package/dist/tm/pty.d.ts.map +0 -1
- package/dist/tm/pty.js +0 -245
- package/dist/tm/pty.js.map +0 -1
- package/dist/tool-coerce.d.ts +0 -24
- package/dist/tool-coerce.d.ts.map +0 -1
- package/dist/tool-coerce.js +0 -63
- package/dist/tool-coerce.js.map +0 -1
package/dist/tm/ptc.js
DELETED
|
@@ -1,865 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tm_ptc_run — programmatic batch orchestration (M1–M3 all shipped).
|
|
3
|
-
*
|
|
4
|
-
* Design: .git/opencode-team/.../ptc-run/02-architect-ptc-run-design.md §2/§3/§4/§6.
|
|
5
|
-
* M1 contract: args schema, the frozen `PtcEngine` seam, the StepGate failure
|
|
6
|
-
* governance (error / call / time budgets, ≤1 retry on idempotent phases,
|
|
7
|
-
* produced-output preservation), the five-state status enum, the char-pinned
|
|
8
|
-
* aggregation summary, composite step numbers (`sXXXX.kNN`) and the
|
|
9
|
-
* trajectory event shapes.
|
|
10
|
-
* M2 engines: WorkerEngine (worker_threads + MessagePort RPC, env:{},
|
|
11
|
-
* resourceLimits, hard `terminate()` wall-clock kill; bootstrap as a
|
|
12
|
-
* build-time string), InlineVmEngine (node:vm runInNewContext fallback —
|
|
13
|
-
* the script timeout kills pre-await synchronous busy-loops; a post-await
|
|
14
|
-
* busy loop blocks the host event loop irrecoverably, documented —
|
|
15
|
-
* prefer worker/auto), InlineSequentialEngine (M1 legacy, test-only).
|
|
16
|
-
* staticPscan is wired as
|
|
17
|
-
* the FIRST gate — banned tokens reject before any engine runs (in both
|
|
18
|
-
* runPtc and the tool execute path).
|
|
19
|
-
* M3 registration: tm/index merges tm_ptc_run into the `tools` map; since
|
|
20
|
-
* v1.5.4 ALL SIX agents carry the allow (team included — CHANGELOG 1.5.4).
|
|
21
|
-
*
|
|
22
|
-
* Engine selection: TM_PTC_ENGINE=auto (default) | worker | inline. auto
|
|
23
|
-
* runs worker-first; an engine-error re-runs the WHOLE program on
|
|
24
|
-
* InlineVmEngine (bridged calls are read-only, so a re-run is safe; the
|
|
25
|
-
* fallback starts with fresh call/error budgets under the SAME wall-clock
|
|
26
|
-
* deadline) and marks `degraded-engine` in the summary.
|
|
27
|
-
*
|
|
28
|
-
* Governance is REUSED, never forked (D6 lock): the inline bridge calls the
|
|
29
|
-
* exact four exported pipelines (`buildPipelines`), so threshold offload,
|
|
30
|
-
* P2/P3, R6 and handle/TTL semantics apply to every bridged call verbatim.
|
|
31
|
-
* PTC is NOT a bypass layer.
|
|
32
|
-
*/
|
|
33
|
-
import * as crypto from "node:crypto";
|
|
34
|
-
import * as vm from "node:vm";
|
|
35
|
-
import { estimateTokens, shouldOffload, shorten } from "./config.js";
|
|
36
|
-
import { buildRef } from "./refs.js";
|
|
37
|
-
import { detectContentType } from "./preview.js";
|
|
38
|
-
import { tmError } from "./tools.js";
|
|
39
|
-
// ---------- public contract types (frozen for M2's RPC surface) ----------
|
|
40
|
-
/** The five PTC run statuses (design §2). */
|
|
41
|
-
export const PTC_STATUS_VALUES = [
|
|
42
|
-
"ok",
|
|
43
|
-
"stopped-error-budget",
|
|
44
|
-
"stopped-call-budget",
|
|
45
|
-
"timeout",
|
|
46
|
-
"engine-error",
|
|
47
|
-
];
|
|
48
|
-
/** Bridge allow set — read-only; tm_ptc_run itself is excluded (no nesting). */
|
|
49
|
-
export const BRIDGE_ALLOW = ["tm_read", "tm_grep", "tm_bash", "tm_fetch"];
|
|
50
|
-
/** Phases safe to retry once (idempotent read-only bridges; design §4.2). */
|
|
51
|
-
export const RETRYABLE_PHASES = ["client", "execute", "store"];
|
|
52
|
-
/**
|
|
53
|
-
* Clamp a caller-supplied budget against the resolved env ceiling. Absent ->
|
|
54
|
-
* the env ceiling; above the ceiling -> clamped DOWN (cannot loosen); below
|
|
55
|
-
* the hard floor -> clamped UP to the floor. Non-finite is ignored (ceiling).
|
|
56
|
-
*/
|
|
57
|
-
function clampBudget(raw, ceiling, floor) {
|
|
58
|
-
const n = typeof raw === "number" ? raw : raw == null ? NaN : Number(raw);
|
|
59
|
-
if (!Number.isFinite(n))
|
|
60
|
-
return ceiling;
|
|
61
|
-
const i = Math.trunc(n);
|
|
62
|
-
if (i > ceiling)
|
|
63
|
-
return ceiling;
|
|
64
|
-
if (i < floor)
|
|
65
|
-
return floor;
|
|
66
|
-
return i;
|
|
67
|
-
}
|
|
68
|
-
export function resolvePtcBudgets(cfg, budgets) {
|
|
69
|
-
const b = budgets && typeof budgets === "object" ? budgets : {};
|
|
70
|
-
return {
|
|
71
|
-
maxCalls: clampBudget(b.max_calls, cfg.ptcMaxCalls, PTC_BUDGET_BOUNDS_FLOOR.maxCalls),
|
|
72
|
-
maxErrors: clampBudget(b.max_errors, cfg.ptcMaxErrors, PTC_BUDGET_BOUNDS_FLOOR.maxErrors),
|
|
73
|
-
timeoutMs: clampBudget(b.timeout_ms, cfg.ptcTimeoutMs, PTC_BUDGET_BOUNDS_FLOOR.timeoutMs),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
// floors mirror config.ts PTC_BUDGET_BOUNDS (kept here so this module stays
|
|
77
|
-
// self-contained for clamp tests without importing config internals).
|
|
78
|
-
const PTC_BUDGET_BOUNDS_FLOOR = { maxCalls: 1, maxErrors: 1, timeoutMs: 5000 };
|
|
79
|
-
// ---------- arg schema + validation ----------
|
|
80
|
-
export const PTC_LABEL_MAX = 80;
|
|
81
|
-
/**
|
|
82
|
-
* Validate + normalize tm_ptc_run args. `program` over the length cap is a
|
|
83
|
-
* hard args error (a program cannot be meaningfully truncated); `label` over
|
|
84
|
-
* 80 is truncated; `budgets` are clamped toward the env ceilings.
|
|
85
|
-
*/
|
|
86
|
-
export function parsePtcArgs(raw, cfg) {
|
|
87
|
-
const a = raw ?? {};
|
|
88
|
-
const program = typeof a.program === "string" ? a.program : "";
|
|
89
|
-
if (!program.trim()) {
|
|
90
|
-
return { ok: false, error: tmError("tm_ptc_run", "args", "缺少 program 参数(async 函数体,可用 tm.read/tm.grep/tm.bash/tm.fetch)") };
|
|
91
|
-
}
|
|
92
|
-
if (program.length > cfg.ptcMaxProgramChars) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
error: tmError("tm_ptc_run", "args", `program 超过长度上限 ${cfg.ptcMaxProgramChars} 字符(实际 ${program.length})。请拆分批次或调高 TM_PTC_MAX_PROGRAM_CHARS。`),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
let label = typeof a.label === "string" && a.label.trim() ? a.label.trim() : "ptc-run";
|
|
99
|
-
if (label.length > PTC_LABEL_MAX)
|
|
100
|
-
label = label.slice(0, PTC_LABEL_MAX);
|
|
101
|
-
const budgets = resolvePtcBudgets(cfg, a.budgets);
|
|
102
|
-
return { ok: true, args: { program, label, budgets } };
|
|
103
|
-
}
|
|
104
|
-
// ---------- static pre-scan (the FIRST gate: wired into runPtc + the tool) --
|
|
105
|
-
/** Banned identifier tokens for the program source (auxiliary guard, not a
|
|
106
|
-
* security boundary — see design §3 trust model). */
|
|
107
|
-
const PSCAN_PATTERNS = [
|
|
108
|
-
/\brequire\b/,
|
|
109
|
-
/\bimport\b/,
|
|
110
|
-
/\bprocess\b/,
|
|
111
|
-
/\bglobalThis\b/,
|
|
112
|
-
/\bDeno\b/,
|
|
113
|
-
/\bBun\b/,
|
|
114
|
-
/\bfs\b/,
|
|
115
|
-
/\bnet\b/,
|
|
116
|
-
/\bchild_process\b/,
|
|
117
|
-
];
|
|
118
|
-
/** Static scan of a program string — the FIRST gate: runPtc and
|
|
119
|
-
* buildPtcRunTool both reject a rejected program before any engine runs. */
|
|
120
|
-
export function staticPscan(program) {
|
|
121
|
-
const tokens = [];
|
|
122
|
-
for (const re of PSCAN_PATTERNS) {
|
|
123
|
-
if (re.test(program))
|
|
124
|
-
tokens.push(re.source.replace(/\\b/g, "").replace(/\b/g, ""));
|
|
125
|
-
}
|
|
126
|
-
return { rejected: tokens.length > 0, tokens };
|
|
127
|
-
}
|
|
128
|
-
// ---------- the inline sequential engine (M1's only engine) ----------------
|
|
129
|
-
class PtcStopSignal extends Error {
|
|
130
|
-
reason;
|
|
131
|
-
constructor(reason) {
|
|
132
|
-
super(`ptc-stop:${reason}`);
|
|
133
|
-
this.reason = reason;
|
|
134
|
-
this.name = "PtcStopSignal";
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function makeFacade(bridge) {
|
|
138
|
-
const one = (tool) => (args) => bridge.call(tool, args && typeof args === "object" ? args : {});
|
|
139
|
-
return { read: one("tm_read"), grep: one("tm_grep"), bash: one("tm_bash"), fetch: one("tm_fetch") };
|
|
140
|
-
}
|
|
141
|
-
/** Best-effort line extraction from a program-side thrown error stack. */
|
|
142
|
-
function extractLine(err) {
|
|
143
|
-
const s = err instanceof Error ? err.stack ?? "" : String(err ?? "");
|
|
144
|
-
const m = /:(\d+):/.exec(s);
|
|
145
|
-
if (m) {
|
|
146
|
-
const n = Number(m[1]);
|
|
147
|
-
if (Number.isFinite(n))
|
|
148
|
-
return n;
|
|
149
|
-
}
|
|
150
|
-
const m2 = /line[:#]?\s*(\d+)/i.exec(s);
|
|
151
|
-
return m2 ? Number(m2[1]) : undefined;
|
|
152
|
-
}
|
|
153
|
-
export class InlineSequentialEngine {
|
|
154
|
-
name = "inline";
|
|
155
|
-
async run(program, bridge, signal) {
|
|
156
|
-
// M1 legacy engine, kept test-only: the program runs as a trusted async
|
|
157
|
-
// function body on the MAIN thread — no sandbox, no timeout, and a
|
|
158
|
-
// post-await busy loop blocks the host event loop irrecoverably (same
|
|
159
|
-
// residual as InlineVmEngine). Production runs WorkerEngine (auto
|
|
160
|
-
// default) or InlineVmEngine.
|
|
161
|
-
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
162
|
-
const tm = makeFacade(bridge);
|
|
163
|
-
let abortHandler;
|
|
164
|
-
const abortP = new Promise((_, rej) => {
|
|
165
|
-
// The driver only aborts on wall-clock timeout (budget stops flow through
|
|
166
|
-
// the gate as PtcStopSignal). M2's worker engine adds terminate here.
|
|
167
|
-
abortHandler = () => rej(new PtcStopSignal("timeout"));
|
|
168
|
-
if (signal.aborted)
|
|
169
|
-
abortHandler();
|
|
170
|
-
else
|
|
171
|
-
signal.addEventListener("abort", abortHandler, { once: true });
|
|
172
|
-
});
|
|
173
|
-
try {
|
|
174
|
-
const fn = new AsyncFunction("tm", `"use strict";\n${program}`);
|
|
175
|
-
return await Promise.race([fn(tm), abortP]);
|
|
176
|
-
}
|
|
177
|
-
finally {
|
|
178
|
-
if (abortHandler)
|
|
179
|
-
signal.removeEventListener("abort", abortHandler);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// ---------- M2: worker engine (MessagePort RPC over worker_threads) ----------
|
|
184
|
-
/**
|
|
185
|
-
* Worker engine — runs the program in a dedicated `worker_threads.Worker`
|
|
186
|
-
* with `env:{}` (process.env emptied), `resourceLimits`, and `terminate()`
|
|
187
|
-
* for hard timeout kills. The bootstrap source is a build-time template
|
|
188
|
-
* string constant (zero runtime file IO, zero new deps, passes tsc).
|
|
189
|
-
*
|
|
190
|
-
* Probe result (P1, %TEMP%): ALL-GREEN on Node 24 / win32 — eval bootstrap,
|
|
191
|
-
* postMessage bidirectional, terminate (9 ms), resourceLimits, env:{} all
|
|
192
|
-
* verified. Auto chain = worker-first, inline fallback.
|
|
193
|
-
*/
|
|
194
|
-
export class WorkerEngine {
|
|
195
|
-
name = "worker";
|
|
196
|
-
async run(program, bridge, signal) {
|
|
197
|
-
const { Worker } = await import("node:worker_threads");
|
|
198
|
-
// Build the worker bootstrap source as a regular string (ts-safe).
|
|
199
|
-
// The worker receives {program, startDeadlineMs} via its first message,
|
|
200
|
-
// creates the tm facade, runs the program, and sends RPC call requests
|
|
201
|
-
// back to the driver which executes them through the gate bridge.
|
|
202
|
-
const src = [
|
|
203
|
-
'"use strict";',
|
|
204
|
-
'const { parentPort } = require("node:worker_threads");',
|
|
205
|
-
"parentPort.on(" + JSON.stringify("message") + ", async function onMsg(msg) {",
|
|
206
|
-
" if (msg && msg.t === " + JSON.stringify("run") + ") {",
|
|
207
|
-
" const program = msg.program;",
|
|
208
|
-
" const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;",
|
|
209
|
-
" const callMap = new Map();",
|
|
210
|
-
" let nextId = 0;",
|
|
211
|
-
' const one = function(tool) { return function(args) { return new Promise(function(resolve, reject) {',
|
|
212
|
-
" const id = nextId++;",
|
|
213
|
-
" callMap.set(id, { resolve: resolve, reject: reject });",
|
|
214
|
-
' parentPort.postMessage({ t: "call", id: id, tool: tool, args: args || {} });',
|
|
215
|
-
" }); }; };",
|
|
216
|
-
" var tm = { read: one(" + JSON.stringify("tm_read") + "), grep: one(" + JSON.stringify("tm_grep") + "), bash: one(" + JSON.stringify("tm_bash") + "), fetch: one(" + JSON.stringify("tm_fetch") + ") };",
|
|
217
|
-
' parentPort.on("message", function(inner) {',
|
|
218
|
-
' if (inner && inner.t === "result" && callMap.has(inner.id)) {',
|
|
219
|
-
" callMap.get(inner.id).resolve(inner.r);",
|
|
220
|
-
" callMap.delete(inner.id);",
|
|
221
|
-
' } else if (inner && inner.t === "abort") {',
|
|
222
|
-
" for (const e of callMap.values()) e.reject(new Error(" + JSON.stringify("ptc-stop:") + " + inner.reason));",
|
|
223
|
-
" callMap.clear();",
|
|
224
|
-
" }",
|
|
225
|
-
" });",
|
|
226
|
-
" try {",
|
|
227
|
-
// strict-mode body, same as the inline engines (they prefix the program
|
|
228
|
-
// with "use strict"; the worker builds the body string here)
|
|
229
|
-
" var body = " + JSON.stringify('"use strict";\n') + " + program;",
|
|
230
|
-
" var fn = new AsyncFunction(" + JSON.stringify("tm") + ", body);",
|
|
231
|
-
" var result = await fn(tm);",
|
|
232
|
-
' parentPort.postMessage({ t: "done", result: result });',
|
|
233
|
-
" } catch (err) {",
|
|
234
|
-
' parentPort.postMessage({ t: "error", message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined });',
|
|
235
|
-
" }",
|
|
236
|
-
" }",
|
|
237
|
-
"});",
|
|
238
|
-
].join("\n");
|
|
239
|
-
return new Promise((resolve, reject) => {
|
|
240
|
-
let settled = false;
|
|
241
|
-
const w = new Worker(src, {
|
|
242
|
-
eval: true,
|
|
243
|
-
env: {},
|
|
244
|
-
resourceLimits: { maxOldGenerationSizeMb: 128, maxYoungGenerationSizeMb: 16 },
|
|
245
|
-
});
|
|
246
|
-
function settle(fn) {
|
|
247
|
-
if (settled)
|
|
248
|
-
return;
|
|
249
|
-
settled = true;
|
|
250
|
-
fn();
|
|
251
|
-
}
|
|
252
|
-
w.on("message", (msg) => {
|
|
253
|
-
if (!msg || settled)
|
|
254
|
-
return;
|
|
255
|
-
if (msg.t === "done") {
|
|
256
|
-
settle(() => {
|
|
257
|
-
w.terminate();
|
|
258
|
-
resolve(msg.result);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
else if (msg.t === "error") {
|
|
262
|
-
settle(() => {
|
|
263
|
-
w.terminate();
|
|
264
|
-
const err = new Error(typeof msg.message === "string" ? msg.message : "worker error");
|
|
265
|
-
if (typeof msg.stack === "string")
|
|
266
|
-
err.stack = msg.stack;
|
|
267
|
-
reject(err);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
else if (msg.t === "call") {
|
|
271
|
-
// Driver executes the bridge call through the gate and replies.
|
|
272
|
-
const req = msg;
|
|
273
|
-
bridge
|
|
274
|
-
.call(req.tool, req.args)
|
|
275
|
-
.then((r) => {
|
|
276
|
-
if (!settled) {
|
|
277
|
-
w.postMessage({ t: "result", id: req.id, r });
|
|
278
|
-
}
|
|
279
|
-
})
|
|
280
|
-
.catch(() => {
|
|
281
|
-
if (!settled) {
|
|
282
|
-
w.postMessage({
|
|
283
|
-
t: "result",
|
|
284
|
-
id: req.id,
|
|
285
|
-
r: { ok: false, error: { tool: req.tool, phase: "execute", message: "bridge call failed" } },
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
w.on("error", (err) => {
|
|
292
|
-
settle(() => reject(err instanceof Error ? err : new Error(String(err))));
|
|
293
|
-
});
|
|
294
|
-
// Wire signal abort → terminate the worker.
|
|
295
|
-
const onAbort = () => {
|
|
296
|
-
settle(() => {
|
|
297
|
-
w.terminate().then(() => reject(new PtcStopSignal("timeout")));
|
|
298
|
-
});
|
|
299
|
-
};
|
|
300
|
-
if (signal.aborted)
|
|
301
|
-
onAbort();
|
|
302
|
-
else
|
|
303
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
304
|
-
// Send the run request.
|
|
305
|
-
w.postMessage({ t: "run", program });
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// ---------- M2: inline-vm fallback engine (node:vm with timeout) ----------
|
|
310
|
-
/**
|
|
311
|
-
* Inline-vm engine — runs the program inside `node:vm` via
|
|
312
|
-
* `runInNewContext` with a script-level execution timeout. Measured on
|
|
313
|
-
* Node 24/win32: `compileFunction`'s `timeout` does NOT interrupt a busy
|
|
314
|
-
* loop when the compiled function is invoked (the call blocks the main
|
|
315
|
-
* thread forever), while the async-IIFE + `runInNewContext` shape DOES —
|
|
316
|
-
* a synchronous busy loop before the first `await` is killed ("Script
|
|
317
|
-
* execution timed out").
|
|
318
|
-
*
|
|
319
|
-
* KNOWN RESIDUAL RISK (design §3, declared): a busy loop AFTER the first
|
|
320
|
-
* `await` runs in a microtask continuation that blocks the host event
|
|
321
|
-
* loop — no timer can fire, so NOTHING reclaims the main thread. This is
|
|
322
|
-
* inherent to any main-thread execution (InlineSequentialEngine shares
|
|
323
|
-
* it); the primary worker engine does not share it (`terminate()` runs on
|
|
324
|
-
* a separate thread) — prefer worker/auto in production.
|
|
325
|
-
*/
|
|
326
|
-
export class InlineVmEngine {
|
|
327
|
-
name = "inline";
|
|
328
|
-
/** Script execution timeout in ms (kills pre-await synchronous
|
|
329
|
-
* busy-loops). Injectable so tests can pin the kill fast. */
|
|
330
|
-
compileTimeoutMs;
|
|
331
|
-
constructor(compileTimeoutMs = 30_000) {
|
|
332
|
-
this.compileTimeoutMs = compileTimeoutMs;
|
|
333
|
-
}
|
|
334
|
-
async run(program, bridge, signal) {
|
|
335
|
-
const tm = makeFacade(bridge);
|
|
336
|
-
let abortHandler;
|
|
337
|
-
const abortP = new Promise((_, rej) => {
|
|
338
|
-
abortHandler = () => rej(new PtcStopSignal("timeout"));
|
|
339
|
-
if (signal.aborted)
|
|
340
|
-
abortHandler();
|
|
341
|
-
else
|
|
342
|
-
signal.addEventListener("abort", abortHandler, { once: true });
|
|
343
|
-
});
|
|
344
|
-
try {
|
|
345
|
-
// The async-IIFE is the program wrapper: `return` stays legal in the
|
|
346
|
-
// body, the `tm` facade rides the context globals, and the script's
|
|
347
|
-
// completion value IS the program's promise. The timeout kills
|
|
348
|
-
// pre-await synchronous busy-loops (see the class doc).
|
|
349
|
-
const promise = vm.runInNewContext(`(async () => {\n"use strict";\n${program}\n})()`, { tm }, { timeout: this.compileTimeoutMs });
|
|
350
|
-
return await Promise.race([promise, abortP]);
|
|
351
|
-
}
|
|
352
|
-
finally {
|
|
353
|
-
if (abortHandler)
|
|
354
|
-
signal.removeEventListener("abort", abortHandler);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Wrap a raw bridge with the StepGate. Every call: enforce time/call budget
|
|
360
|
-
* (throw a stop that unwinds the program), assign a composite step id
|
|
361
|
-
* `parent.kNN`, retry ≤1 on idempotent phases, record + persist errors, and
|
|
362
|
-
* trip the error budget. The gate lives ONLY here (outside the engine), per
|
|
363
|
-
* design §3 ("决策进代码").
|
|
364
|
-
*/
|
|
365
|
-
export function createGateBridge(raw, parent, budgets, state, opts) {
|
|
366
|
-
const { now, deadlineAt, store } = opts;
|
|
367
|
-
const ptcTag = opts.trajectoryParent ?? parent;
|
|
368
|
-
let seq = 0;
|
|
369
|
-
const traj = (e) => store?.appendTrajectory(e);
|
|
370
|
-
return {
|
|
371
|
-
async call(tool, args) {
|
|
372
|
-
if (state.stopReason)
|
|
373
|
-
throw new PtcStopSignal(state.stopReason);
|
|
374
|
-
// time budget checked BEFORE the call so a run cannot start a call it
|
|
375
|
-
// has no time left for.
|
|
376
|
-
if (now() >= deadlineAt) {
|
|
377
|
-
state.stopReason = "timeout";
|
|
378
|
-
throw new PtcStopSignal("timeout");
|
|
379
|
-
}
|
|
380
|
-
// call budget: only `maxCalls` bridged calls may be dispatched.
|
|
381
|
-
if (state.calls >= budgets.maxCalls) {
|
|
382
|
-
state.stopReason = "call-budget";
|
|
383
|
-
throw new PtcStopSignal("call-budget");
|
|
384
|
-
}
|
|
385
|
-
state.calls++;
|
|
386
|
-
seq++;
|
|
387
|
-
const composite = `${parent}.k${String(seq).padStart(2, "0")}`;
|
|
388
|
-
traj({ tool, step_id: composite, event: "call", ptc: ptcTag });
|
|
389
|
-
const t0 = now();
|
|
390
|
-
let res = await raw.call(tool, args);
|
|
391
|
-
let retried = false;
|
|
392
|
-
if (!res.ok && RETRYABLE_PHASES.includes(res.error.phase)) {
|
|
393
|
-
retried = true;
|
|
394
|
-
state.retries++;
|
|
395
|
-
res = await raw.call(tool, args);
|
|
396
|
-
}
|
|
397
|
-
const ms = now() - t0;
|
|
398
|
-
if (res.ok) {
|
|
399
|
-
const { tokens, dest, ref } = describeOk(res.data);
|
|
400
|
-
state.steps.push({ n: seq, tool, ok: true, ms, tokens, dest, ref });
|
|
401
|
-
traj({
|
|
402
|
-
tool,
|
|
403
|
-
step_id: composite,
|
|
404
|
-
event: "result",
|
|
405
|
-
offloaded: Boolean(ref),
|
|
406
|
-
tokens,
|
|
407
|
-
ptc: ptcTag,
|
|
408
|
-
...(ref ? { ref } : {}),
|
|
409
|
-
});
|
|
410
|
-
return res;
|
|
411
|
-
}
|
|
412
|
-
// error path — full text persists to steps/sXXXX.kNN/ (design §4.1)
|
|
413
|
-
state.errors++;
|
|
414
|
-
const err = res.error;
|
|
415
|
-
let errorRef;
|
|
416
|
-
if (store) {
|
|
417
|
-
try {
|
|
418
|
-
store.writeResult(composite, {
|
|
419
|
-
tool,
|
|
420
|
-
content: JSON.stringify({ error: err }, null, 2),
|
|
421
|
-
tokens: estimateTokens(JSON.stringify(err)),
|
|
422
|
-
contentType: "json",
|
|
423
|
-
preview: shorten(err.message, 120),
|
|
424
|
-
expireAt: Date.now() + 24 * 60 * 60 * 1000,
|
|
425
|
-
});
|
|
426
|
-
errorRef = buildRef(store.runId, composite);
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
/* error persistence is best-effort; never mask the original error */
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
state.steps.push({
|
|
433
|
-
n: seq,
|
|
434
|
-
tool,
|
|
435
|
-
ok: false,
|
|
436
|
-
ms,
|
|
437
|
-
tokens: 0,
|
|
438
|
-
dest: "err",
|
|
439
|
-
phase: err.phase,
|
|
440
|
-
line: err.line,
|
|
441
|
-
retry: retried,
|
|
442
|
-
message: err.message,
|
|
443
|
-
errorRef,
|
|
444
|
-
});
|
|
445
|
-
traj({
|
|
446
|
-
tool,
|
|
447
|
-
step_id: composite,
|
|
448
|
-
event: "error",
|
|
449
|
-
phase: err.phase,
|
|
450
|
-
...(typeof err.line === "number" ? { line: err.line } : {}),
|
|
451
|
-
retry: retried,
|
|
452
|
-
ptc: ptcTag,
|
|
453
|
-
...(errorRef ? { ref: errorRef } : {}),
|
|
454
|
-
});
|
|
455
|
-
if (state.errors >= budgets.maxErrors) {
|
|
456
|
-
state.stopReason = "error-budget";
|
|
457
|
-
throw new PtcStopSignal("error-budget");
|
|
458
|
-
}
|
|
459
|
-
return res;
|
|
460
|
-
},
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
/** Describe an ok bridge result for the success table (inline vs handle). */
|
|
464
|
-
function describeOk(data) {
|
|
465
|
-
if (data && typeof data === "object" && data.offloaded === true) {
|
|
466
|
-
const o = data;
|
|
467
|
-
const ref = typeof o.ref === "string" ? o.ref : null;
|
|
468
|
-
return {
|
|
469
|
-
tokens: typeof o.tokens === "number" ? o.tokens : 0,
|
|
470
|
-
dest: ref ? `ref:${shortRefCode(ref)}` : "ref",
|
|
471
|
-
ref,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
const text = typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
475
|
-
return { tokens: estimateTokens(text), dest: "inline", ref: null };
|
|
476
|
-
}
|
|
477
|
-
/** Short display code for a ref: the `steps/<id>` step segment. */
|
|
478
|
-
function shortRefCode(ref) {
|
|
479
|
-
const m = /\/steps\/([^/]+)\/result/.exec(ref);
|
|
480
|
-
return m ? m[1] : shorten(ref, 24);
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* Select the PTC engine based on the TM_PTC_ENGINE mode.
|
|
484
|
-
* - "worker": WorkerEngine only (throws on failure → engine-error status).
|
|
485
|
-
* - "inline": InlineVmEngine only (vm timeout for sync busy-loops).
|
|
486
|
-
* - "auto" (default): try WorkerEngine first; on failure → InlineVmEngine
|
|
487
|
-
* with `degraded=true` (marked `degraded-engine` in the summary).
|
|
488
|
-
*
|
|
489
|
-
* Probe result (P1, %TEMP%): ALL-GREEN on Node 24/win32 — auto chain =
|
|
490
|
-
* worker-first, inline fallback. Design §3 table.
|
|
491
|
-
*/
|
|
492
|
-
export function selectEngine(mode, bridge) {
|
|
493
|
-
if (mode === "inline") {
|
|
494
|
-
return { engine: new InlineVmEngine(), degraded: false };
|
|
495
|
-
}
|
|
496
|
-
if (mode === "worker") {
|
|
497
|
-
return { engine: new WorkerEngine(), degraded: false };
|
|
498
|
-
}
|
|
499
|
-
// auto: worker first. Construction never throws (no spawn until run()),
|
|
500
|
-
// so this branch cannot fail — the REAL auto-degrade happens in runPtc,
|
|
501
|
-
// which re-runs the whole program on InlineVmEngine when a worker run
|
|
502
|
-
// returns engine-error (bridged calls are read-only, so a re-run is safe;
|
|
503
|
-
// the fallback run starts with fresh call/error budgets under the SAME
|
|
504
|
-
// wall-clock deadline).
|
|
505
|
-
return { engine: new WorkerEngine(), degraded: false };
|
|
506
|
-
}
|
|
507
|
-
export async function runPtc(o) {
|
|
508
|
-
const now = o.now ?? Date.now;
|
|
509
|
-
const start = now();
|
|
510
|
-
const deadlineAt = start + o.budgets.timeoutMs;
|
|
511
|
-
// M2: engine selection — explicit override or auto/worker/inline from config.
|
|
512
|
-
let engine;
|
|
513
|
-
let degraded = false;
|
|
514
|
-
if (o.engine) {
|
|
515
|
-
engine = o.engine;
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
const sel = selectEngine(o.cfg.ptcEngine, o.bridge);
|
|
519
|
-
engine = sel.engine;
|
|
520
|
-
degraded = sel.degraded;
|
|
521
|
-
}
|
|
522
|
-
// M2: static pre-scan — reject programs with banned tokens before any
|
|
523
|
-
// engine runs. Shape: engine-error with the pscan tokens listed.
|
|
524
|
-
// (Design §3: "辅助手段,声明不作安全边界".)
|
|
525
|
-
const pscan = staticPscan(o.program);
|
|
526
|
-
if (pscan.rejected) {
|
|
527
|
-
return {
|
|
528
|
-
label: o.label,
|
|
529
|
-
status: "engine-error",
|
|
530
|
-
steps: [],
|
|
531
|
-
okCount: 0,
|
|
532
|
-
errCount: 0,
|
|
533
|
-
retries: 0,
|
|
534
|
-
calls: 0,
|
|
535
|
-
ms: 0,
|
|
536
|
-
engine: engine.name,
|
|
537
|
-
degraded,
|
|
538
|
-
returnValue: undefined,
|
|
539
|
-
returned: false,
|
|
540
|
-
engineError: {
|
|
541
|
-
tool: "tm_ptc_run",
|
|
542
|
-
phase: "args",
|
|
543
|
-
message: `程序包含禁止标识符(${pscan.tokens.join(", ")}),已拒绝执行。`,
|
|
544
|
-
},
|
|
545
|
-
parentStepId: o.parentStepId,
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
// Auto-degrade: if the selected engine throws (not a PtcStopSignal),
|
|
549
|
-
// and we're in auto mode with no explicit override, retry with InlineVmEngine.
|
|
550
|
-
const tryRun = async (eng) => {
|
|
551
|
-
const state = { calls: 0, errors: 0, retries: 0, steps: [] };
|
|
552
|
-
const gate = createGateBridge(o.bridge, o.parentStepId, o.budgets, state, {
|
|
553
|
-
now,
|
|
554
|
-
deadlineAt,
|
|
555
|
-
store: o.store,
|
|
556
|
-
});
|
|
557
|
-
// parent call event (design §6)
|
|
558
|
-
o.store?.appendTrajectory({
|
|
559
|
-
tool: "tm_ptc_run",
|
|
560
|
-
step_id: o.parentStepId,
|
|
561
|
-
event: "call",
|
|
562
|
-
label: o.label,
|
|
563
|
-
program_sha256: crypto.createHash("sha256").update(o.program).digest("hex"),
|
|
564
|
-
budgets: o.budgets,
|
|
565
|
-
});
|
|
566
|
-
const ac = new AbortController();
|
|
567
|
-
const remaining = Math.max(0, deadlineAt - now());
|
|
568
|
-
const timer = setTimeout(() => ac.abort(), remaining);
|
|
569
|
-
let status;
|
|
570
|
-
let returnValue;
|
|
571
|
-
let returned = false;
|
|
572
|
-
let engineError;
|
|
573
|
-
try {
|
|
574
|
-
returnValue = await eng.run(o.program, gate, ac.signal);
|
|
575
|
-
returned = true;
|
|
576
|
-
status = state.stopReason ? mapStop(state.stopReason) : "ok";
|
|
577
|
-
}
|
|
578
|
-
catch (err) {
|
|
579
|
-
if (err instanceof PtcStopSignal) {
|
|
580
|
-
status = mapStop(err.reason);
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
status = "engine-error";
|
|
584
|
-
const line = extractLine(err);
|
|
585
|
-
engineError = {
|
|
586
|
-
tool: "tm_ptc_run",
|
|
587
|
-
phase: "execute",
|
|
588
|
-
message: err instanceof Error ? err.message : String(err ?? "engine error"),
|
|
589
|
-
...(line != null ? { line } : {}),
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
finally {
|
|
594
|
-
clearTimeout(timer);
|
|
595
|
-
}
|
|
596
|
-
const ms = now() - start;
|
|
597
|
-
const outcome = {
|
|
598
|
-
label: o.label,
|
|
599
|
-
status,
|
|
600
|
-
steps: state.steps,
|
|
601
|
-
okCount: state.steps.filter((s) => s.ok).length,
|
|
602
|
-
errCount: state.steps.filter((s) => !s.ok).length,
|
|
603
|
-
retries: state.retries,
|
|
604
|
-
calls: state.calls,
|
|
605
|
-
ms,
|
|
606
|
-
engine: eng.name,
|
|
607
|
-
degraded,
|
|
608
|
-
returnValue,
|
|
609
|
-
returned,
|
|
610
|
-
...(engineError ? { engineError } : {}),
|
|
611
|
-
parentStepId: o.parentStepId,
|
|
612
|
-
};
|
|
613
|
-
o.store?.appendTrajectory({
|
|
614
|
-
tool: "tm_ptc_run",
|
|
615
|
-
step_id: o.parentStepId,
|
|
616
|
-
event: "finish",
|
|
617
|
-
status: outcome.status,
|
|
618
|
-
calls: outcome.calls,
|
|
619
|
-
errors: outcome.errCount,
|
|
620
|
-
retries: outcome.retries,
|
|
621
|
-
ms: outcome.ms,
|
|
622
|
-
});
|
|
623
|
-
return outcome;
|
|
624
|
-
};
|
|
625
|
-
// Auto mode: try worker first; on engine-error → degrade to inline.
|
|
626
|
-
if (!o.engine && o.cfg.ptcEngine === "auto" && engine.name === "worker") {
|
|
627
|
-
const result = await tryRun(engine);
|
|
628
|
-
if (result.status === "engine-error") {
|
|
629
|
-
// Degrade to inline-vm and re-run.
|
|
630
|
-
const inlineEngine = new InlineVmEngine();
|
|
631
|
-
const fallback = await tryRun(inlineEngine);
|
|
632
|
-
fallback.degraded = true;
|
|
633
|
-
fallback.engine = "inline";
|
|
634
|
-
return fallback;
|
|
635
|
-
}
|
|
636
|
-
return result;
|
|
637
|
-
}
|
|
638
|
-
return tryRun(engine);
|
|
639
|
-
}
|
|
640
|
-
function mapStop(reason) {
|
|
641
|
-
if (reason === "timeout")
|
|
642
|
-
return "timeout";
|
|
643
|
-
if (reason === "error-budget")
|
|
644
|
-
return "stopped-error-budget";
|
|
645
|
-
return "stopped-call-budget";
|
|
646
|
-
}
|
|
647
|
-
// ---------- aggregation summary (design §2 — char-pinned headers) ----------
|
|
648
|
-
export const PTC_SUMMARY_HEADER = "PTC 摘要";
|
|
649
|
-
export const PTC_OK_SECTION = "-- 成功分部(≤8 行,超出整表卸载)";
|
|
650
|
-
export const PTC_OK_HEADER = " # tool ms tokens 落点(inline|ref 短码)";
|
|
651
|
-
export const PTC_ERR_SECTION = "-- 错误分部(全量错误已落 run store)";
|
|
652
|
-
export const PTC_ERR_HEADER = " # tool phase line retry message(截断)";
|
|
653
|
-
export const PTC_RETURN_PREFIX = "返回值: ";
|
|
654
|
-
export const PTC_ERRORFULL_PREFIX = "错误全文: ";
|
|
655
|
-
const OK_ROWS_INLINE_MAX = 8;
|
|
656
|
-
const RETURN_VALUE_MAX = 2000;
|
|
657
|
-
function engineLabel(o) {
|
|
658
|
-
if (o.engine === "worker")
|
|
659
|
-
return "worker";
|
|
660
|
-
return o.degraded ? "inline(degraded)" : "inline";
|
|
661
|
-
}
|
|
662
|
-
/** Render the fixed-shape aggregation summary. `offload` is used to route an
|
|
663
|
-
* oversized success/error table (or return value) to a run-store handle. */
|
|
664
|
-
export function renderPtcSummary(o, offload) {
|
|
665
|
-
const lines = [];
|
|
666
|
-
lines.push(`${PTC_SUMMARY_HEADER} · ${o.label} · status=${o.status}`);
|
|
667
|
-
lines.push(`steps=${o.okCount + o.errCount} ok=${o.okCount} err=${o.errCount} retries=${o.retries} ms=${o.ms} engine=${engineLabel(o)}`);
|
|
668
|
-
const okSteps = o.steps.filter((s) => s.ok);
|
|
669
|
-
const errSteps = o.steps.filter((s) => !s.ok);
|
|
670
|
-
lines.push(PTC_OK_SECTION);
|
|
671
|
-
lines.push(PTC_OK_HEADER);
|
|
672
|
-
if (okSteps.length > OK_ROWS_INLINE_MAX && offload) {
|
|
673
|
-
const table = okSteps.map(okRow).join("\n");
|
|
674
|
-
lines.push(`(成功 ${okSteps.length} 行,超 ${OK_ROWS_INLINE_MAX},整表卸载)`);
|
|
675
|
-
lines.push(offload(table, "ok"));
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
for (const s of okSteps)
|
|
679
|
-
lines.push(okRow(s));
|
|
680
|
-
}
|
|
681
|
-
lines.push(PTC_ERR_SECTION);
|
|
682
|
-
lines.push(PTC_ERR_HEADER);
|
|
683
|
-
for (const s of errSteps)
|
|
684
|
-
lines.push(errRow(s));
|
|
685
|
-
if (o.engineError) {
|
|
686
|
-
lines.push(errRow({
|
|
687
|
-
n: 0,
|
|
688
|
-
tool: "tm_ptc_run",
|
|
689
|
-
phase: o.engineError.phase,
|
|
690
|
-
line: o.engineError.line,
|
|
691
|
-
retry: false,
|
|
692
|
-
message: `程序抛出:${o.engineError.message}`,
|
|
693
|
-
}));
|
|
694
|
-
}
|
|
695
|
-
if (errSteps.length === 0 && !o.engineError)
|
|
696
|
-
lines.push(" (无)");
|
|
697
|
-
// return value (JSON-serialized, capped; oversized → offload handle)
|
|
698
|
-
if (o.returned) {
|
|
699
|
-
const json = safeStringify(o.returnValue);
|
|
700
|
-
if (json.length > RETURN_VALUE_MAX && offload) {
|
|
701
|
-
lines.push(PTC_RETURN_PREFIX + "(超 2000 字符,卸载为句柄)");
|
|
702
|
-
lines.push(offload(json, "return"));
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
lines.push(PTC_RETURN_PREFIX + json);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
else {
|
|
709
|
-
lines.push(PTC_RETURN_PREFIX + "(无:程序未正常 return)");
|
|
710
|
-
}
|
|
711
|
-
// error full-text refs (aggregate handle when many)
|
|
712
|
-
const refs = o.steps.filter((s) => s.errorRef).map((s) => s.errorRef);
|
|
713
|
-
if (refs.length === 0) {
|
|
714
|
-
lines.push(PTC_ERRORFULL_PREFIX + "(无错误落盘)");
|
|
715
|
-
}
|
|
716
|
-
else if (refs.length > 8 && offload) {
|
|
717
|
-
lines.push(PTC_ERRORFULL_PREFIX + offload(refs.join("\n"), "errrefs"));
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
lines.push(PTC_ERRORFULL_PREFIX + refs.map((r) => `tm://…/${shortRefCode(r)}`).join(" "));
|
|
721
|
-
}
|
|
722
|
-
return lines.join("\n");
|
|
723
|
-
}
|
|
724
|
-
function okRow(s) {
|
|
725
|
-
return ` ${s.n} ${pad(s.tool, 8)}${String(s.ms)}ms ${s.tokens} ${s.dest}`;
|
|
726
|
-
}
|
|
727
|
-
function errRow(s) {
|
|
728
|
-
const phase = s.phase ?? "-";
|
|
729
|
-
const line = typeof s.line === "number" ? String(s.line) : "-";
|
|
730
|
-
const retry = s.retry ? "yes" : "no";
|
|
731
|
-
const msg = shorten(s.message ?? "", 60);
|
|
732
|
-
return ` ${s.n} ${pad(s.tool, 8)}${pad(phase, 11)}${pad(line, 5)}${pad(retry, 5)}${msg}`;
|
|
733
|
-
}
|
|
734
|
-
function pad(v, w) {
|
|
735
|
-
const s = String(v);
|
|
736
|
-
return s.length >= w ? s + " " : s + " ".repeat(w - s.length);
|
|
737
|
-
}
|
|
738
|
-
function safeStringify(v) {
|
|
739
|
-
try {
|
|
740
|
-
const s = JSON.stringify(v);
|
|
741
|
-
return s === undefined ? "null" : s;
|
|
742
|
-
}
|
|
743
|
-
catch {
|
|
744
|
-
return String(v);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
// ---------- bridge over the real governed pipelines ------------------------
|
|
748
|
-
/**
|
|
749
|
-
* Real M1 bridge: delegate each call to the corresponding exported pipeline,
|
|
750
|
-
* `ctx` passed through untouched, and normalize the pipeline's `unknown`
|
|
751
|
-
* result into `PtcCallResult` (TmErrorBody -> ok:false; anything else -> the
|
|
752
|
-
* already-governed data). Governance is entirely the pipelines'.
|
|
753
|
-
*/
|
|
754
|
-
export function pipelineBridge(pipelines, ctx) {
|
|
755
|
-
const pick = (tool) => tool === "tm_read"
|
|
756
|
-
? pipelines.tmRead
|
|
757
|
-
: tool === "tm_grep"
|
|
758
|
-
? pipelines.tmGrep
|
|
759
|
-
: tool === "tm_bash"
|
|
760
|
-
? pipelines.tmBash
|
|
761
|
-
: pipelines.tmFetch;
|
|
762
|
-
return {
|
|
763
|
-
async call(tool, args) {
|
|
764
|
-
if (!BRIDGE_ALLOW.includes(tool)) {
|
|
765
|
-
return { ok: false, error: tmError(tool, "args", "工具不在桥接白名单内").error };
|
|
766
|
-
}
|
|
767
|
-
const res = await pick(tool)(args ?? {}, ctx);
|
|
768
|
-
if (res && typeof res === "object" && "error" in res) {
|
|
769
|
-
return { ok: false, error: res.error };
|
|
770
|
-
}
|
|
771
|
-
return { ok: true, data: res };
|
|
772
|
-
},
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
// ---------- the tool definition (BUILT here, REGISTERED in M3) -------------
|
|
776
|
-
const PTC_RUN_DESCRIPTION = `Program-mode batch orchestration over the four governed tm_* tools. One program, N bridged calls, zero LLM round-trips during the run; only an aggregation summary returns to context.
|
|
777
|
-
|
|
778
|
-
- program: an async function body. Available: \`tm.read(args)\`, \`tm.grep(args)\`, \`tm.bash(args)\`, \`tm.fetch(args)\` — same args as the tm_* four. Each returns \`{ok:true, data}\` (data is already governed: inline text, or an offload handle you can tm.fetch again) or \`{ok:false, error:{tool,phase,line?,message}}\`. \`return\` a value; it is JSON-serialized into the summary (capped 2000 chars, oversized → handle).
|
|
779
|
-
- budgets (optional, tighten-only; clamped to TM_PTC_* ceilings): max_calls, max_errors, timeout_ms. Hitting any budget stops the whole run (produced output is NOT lost). Errors are retried at most once and only on idempotent phases (client/execute/store).
|
|
780
|
-
- Governance is NOT bypassed: every bridged call runs the full tm_* pipeline (P2 path scope, P3 allowlist, R6, threshold offload + handles, TTL). Status is one of ok | stopped-error-budget | stopped-call-budget | timeout | engine-error.
|
|
781
|
-
- Programs containing require/import/process/globalThis/Deno/Bun/fs/net/child_process are rejected before execution (static pre-scan, auxiliary guard).
|
|
782
|
-
- Engine: TM_PTC_ENGINE=auto|worker|inline (auto tries worker first; on engine failure it re-runs the WHOLE program on inline with a degraded-engine mark — bridged calls are read-only, so a re-run is safe, and the fallback starts with fresh budgets under the same wall-clock deadline). The inline engine kills pre-await synchronous busy-loops via the vm script timeout; a busy loop after the first await blocks the host event loop irrecoverably — prefer worker/auto.`;
|
|
783
|
-
/** Build the tm_ptc_run ToolDefinition. Registered in the tool segment
|
|
784
|
-
* when the agent's whitelist grants tm_ptc_run (M3). `deps.args` is the
|
|
785
|
-
* ZodRawShape from tools.buildPtcArgsSchema() when zod is available — the
|
|
786
|
-
* descriptor fallback below keeps the tool usable without zod. */
|
|
787
|
-
export function buildPtcRunTool(deps) {
|
|
788
|
-
const { cfg, store, nextStepId, ctx, accessToken } = deps;
|
|
789
|
-
const baseBridge = deps.bridge ?? (deps.pipelines ? pipelineBridge(deps.pipelines, ctx) : undefined);
|
|
790
|
-
const expireAt = () => Date.now() + cfg.blackboardTtlDays * 24 * 60 * 60 * 1000;
|
|
791
|
-
return {
|
|
792
|
-
description: PTC_RUN_DESCRIPTION,
|
|
793
|
-
args: deps.args ?? {
|
|
794
|
-
program: { descriptor: "program: string (required, async fn body, ≤TM_PTC_MAX_PROGRAM_CHARS)" },
|
|
795
|
-
label: { descriptor: "label: string (optional, ≤80 chars)" },
|
|
796
|
-
budgets: { descriptor: "budgets: { max_calls?, max_errors?, timeout_ms? } (optional, tighten-only)" },
|
|
797
|
-
},
|
|
798
|
-
execute: async (rawArgs) => {
|
|
799
|
-
const parsed = parsePtcArgs(rawArgs ?? {}, cfg);
|
|
800
|
-
if (!parsed.ok) {
|
|
801
|
-
return { output: `[${parsed.error.error.tool} 失败 · phase=${parsed.error.error.phase}]\n${parsed.error.error.message}` };
|
|
802
|
-
}
|
|
803
|
-
// M2: static pre-scan — reject programs with banned tokens before any engine runs.
|
|
804
|
-
const pscan = staticPscan(parsed.args.program);
|
|
805
|
-
if (pscan.rejected) {
|
|
806
|
-
return {
|
|
807
|
-
output: `[tm_ptc_run 失败 · phase=args]\n程序包含禁止标识符(${pscan.tokens.join(", ")}),已拒绝执行。PTC 桥接仅暴露 tm.read/tm.grep/tm.bash/tm.fetch;require/import/process 等不在桥接白名单内。`,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
if (!baseBridge) {
|
|
811
|
-
return {
|
|
812
|
-
output: "[tm_ptc_run 失败 · phase=client]\n桥接管线未提供。",
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
// Generate a fresh parent step ID per call.
|
|
816
|
-
const parentStepId = nextStepId();
|
|
817
|
-
// Per-call offload helper: persist `text` under a PTC sub-step and
|
|
818
|
-
// render a handle block matching the four tools' visible shape.
|
|
819
|
-
const offloadBlock = (text, kind) => {
|
|
820
|
-
const tokens = estimateTokens(text);
|
|
821
|
-
try {
|
|
822
|
-
const stored = store.writeResult(`${parentStepId}.${kind}`, {
|
|
823
|
-
tool: "tm_ptc_run",
|
|
824
|
-
content: text,
|
|
825
|
-
tokens,
|
|
826
|
-
contentType: detectContentType(text),
|
|
827
|
-
preview: shorten(text, 120),
|
|
828
|
-
expireAt: expireAt(),
|
|
829
|
-
});
|
|
830
|
-
return [
|
|
831
|
-
`ref: ${stored.ref}`,
|
|
832
|
-
`access_token: ${accessToken}`,
|
|
833
|
-
`expire_at: ${stored.expireAt}`,
|
|
834
|
-
`tokens: ${tokens}`,
|
|
835
|
-
`(用 tm_fetch(ref, access_token) 取回;大载荷先试 mode:"structure")`,
|
|
836
|
-
].join("\n");
|
|
837
|
-
}
|
|
838
|
-
catch {
|
|
839
|
-
return text.slice(0, 2000);
|
|
840
|
-
}
|
|
841
|
-
};
|
|
842
|
-
const outcome = await runPtc({
|
|
843
|
-
program: parsed.args.program,
|
|
844
|
-
label: parsed.args.label,
|
|
845
|
-
budgets: parsed.args.budgets,
|
|
846
|
-
parentStepId,
|
|
847
|
-
cfg,
|
|
848
|
-
store,
|
|
849
|
-
bridge: baseBridge,
|
|
850
|
-
...(deps.engine ? { engine: deps.engine } : {}),
|
|
851
|
-
});
|
|
852
|
-
const content = renderPtcSummary(outcome, offloadBlock);
|
|
853
|
-
// whole-summary offload if it still exceeds the threshold.
|
|
854
|
-
if (shouldOffload(estimateTokens(content), cfg.offloadThreshold)) {
|
|
855
|
-
return {
|
|
856
|
-
output: `PTC 摘要过大(≈${estimateTokens(content)} tokens),已整体卸载为句柄,短摘要随后:\n` +
|
|
857
|
-
offloadBlock(content, "rpt") +
|
|
858
|
-
`\n--- 摘要前 1200 字符 ---\n${content.slice(0, 1200)}`,
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
return { output: content };
|
|
862
|
-
},
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
//# sourceMappingURL=ptc.js.map
|