@te-river/opencode-team-mode 1.6.0 → 1.6.1
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 +80 -15
- package/README.zh-CN.md +67 -18
- package/dist/host/note.d.ts +7 -0
- package/dist/host/note.d.ts.map +1 -0
- package/dist/host/note.js +28 -0
- package/dist/host/note.js.map +1 -0
- package/dist/host/v1.d.ts +14 -0
- package/dist/host/v1.d.ts.map +1 -0
- package/dist/host/v1.js +340 -0
- package/dist/host/v1.js.map +1 -0
- package/dist/host/v2-browser-gate.d.ts +45 -0
- package/dist/host/v2-browser-gate.d.ts.map +1 -0
- package/dist/host/v2-browser-gate.js +319 -0
- package/dist/host/v2-browser-gate.js.map +1 -0
- package/dist/host/v2-capabilities.d.ts +68 -0
- package/dist/host/v2-capabilities.d.ts.map +1 -0
- package/dist/host/v2-capabilities.js +146 -0
- package/dist/host/v2-capabilities.js.map +1 -0
- package/dist/host/v2-client.d.ts +53 -0
- package/dist/host/v2-client.d.ts.map +1 -0
- package/dist/host/v2-client.js +193 -0
- package/dist/host/v2-client.js.map +1 -0
- package/dist/host/v2-events.d.ts +56 -0
- package/dist/host/v2-events.d.ts.map +1 -0
- package/dist/host/v2-events.js +158 -0
- package/dist/host/v2-events.js.map +1 -0
- package/dist/host/v2-guard.d.ts +101 -0
- package/dist/host/v2-guard.d.ts.map +1 -0
- package/dist/host/v2-guard.js +199 -0
- package/dist/host/v2-guard.js.map +1 -0
- package/dist/host/v2-offload.d.ts +148 -0
- package/dist/host/v2-offload.d.ts.map +1 -0
- package/dist/host/v2-offload.js +288 -0
- package/dist/host/v2-offload.js.map +1 -0
- package/dist/host/v2-permissions.d.ts +123 -0
- package/dist/host/v2-permissions.d.ts.map +1 -0
- package/dist/host/v2-permissions.js +190 -0
- package/dist/host/v2-permissions.js.map +1 -0
- package/dist/host/v2-probe.d.ts +85 -0
- package/dist/host/v2-probe.d.ts.map +1 -0
- package/dist/host/v2-probe.js +268 -0
- package/dist/host/v2-probe.js.map +1 -0
- package/dist/host/v2-scope.d.ts +51 -0
- package/dist/host/v2-scope.d.ts.map +1 -0
- package/dist/host/v2-scope.js +58 -0
- package/dist/host/v2-scope.js.map +1 -0
- package/dist/host/v2-session-client.d.ts +74 -0
- package/dist/host/v2-session-client.d.ts.map +1 -0
- package/dist/host/v2-session-client.js +171 -0
- package/dist/host/v2-session-client.js.map +1 -0
- package/dist/host/v2-session.d.ts +85 -0
- package/dist/host/v2-session.d.ts.map +1 -0
- package/dist/host/v2-session.js +171 -0
- package/dist/host/v2-session.js.map +1 -0
- package/dist/host/v2-subagent.d.ts +157 -0
- package/dist/host/v2-subagent.d.ts.map +1 -0
- package/dist/host/v2-subagent.js +286 -0
- package/dist/host/v2-subagent.js.map +1 -0
- package/dist/host/v2-tool.d.ts +67 -0
- package/dist/host/v2-tool.d.ts.map +1 -0
- package/dist/host/v2-tool.js +209 -0
- package/dist/host/v2-tool.js.map +1 -0
- package/dist/host/v2-transcript.d.ts +73 -0
- package/dist/host/v2-transcript.d.ts.map +1 -0
- package/dist/host/v2-transcript.js +151 -0
- package/dist/host/v2-transcript.js.map +1 -0
- package/dist/host/v2-types.d.ts +229 -0
- package/dist/host/v2-types.d.ts.map +1 -0
- package/dist/host/v2-types.js +17 -0
- package/dist/host/v2-types.js.map +1 -0
- package/dist/host/v2.d.ts +42 -0
- package/dist/host/v2.d.ts.map +1 -0
- package/dist/host/v2.js +706 -0
- package/dist/host/v2.js.map +1 -0
- package/dist/index.d.ts +23 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -366
- package/dist/index.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 +452 -426
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/shared.d.ts +2 -2
- package/dist/prompts/shared.d.ts.map +1 -1
- package/dist/prompts/shared.js +30 -10
- package/dist/prompts/shared.js.map +1 -1
- package/dist/prompts/specialists.d.ts +2 -2
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +7 -6
- package/dist/prompts/specialists.js.map +1 -1
- package/dist/task-offload.d.ts +16 -0
- package/dist/task-offload.d.ts.map +1 -1
- package/dist/task-offload.js +51 -0
- package/dist/task-offload.js.map +1 -1
- package/dist/tm/browser.d.ts.map +1 -1
- package/dist/tm/browser.js +41 -10
- package/dist/tm/browser.js.map +1 -1
- package/dist/tm/dispatch.d.ts +47 -0
- package/dist/tm/dispatch.d.ts.map +1 -1
- package/dist/tm/dispatch.js +230 -31
- package/dist/tm/dispatch.js.map +1 -1
- package/dist/tm/dupe-guard.d.ts +16 -3
- package/dist/tm/dupe-guard.d.ts.map +1 -1
- package/dist/tm/dupe-guard.js +27 -7
- package/dist/tm/dupe-guard.js.map +1 -1
- package/dist/tm/index.d.ts +40 -1
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +40 -11
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +114 -0
- package/dist/tm/ledger.d.ts.map +1 -0
- package/dist/tm/ledger.js +308 -0
- package/dist/tm/ledger.js.map +1 -0
- package/dist/tm/perm-ask.d.ts +1 -3
- package/dist/tm/perm-ask.d.ts.map +1 -1
- package/dist/tm/perm-ask.js +10 -1
- package/dist/tm/perm-ask.js.map +1 -1
- package/dist/tm/preview.d.ts +55 -0
- package/dist/tm/preview.d.ts.map +1 -1
- package/dist/tm/preview.js +225 -0
- package/dist/tm/preview.js.map +1 -1
- package/dist/tm/search.d.ts.map +1 -1
- package/dist/tm/search.js +40 -10
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/serp-loop.d.ts +45 -0
- package/dist/tm/serp-loop.d.ts.map +1 -0
- package/dist/tm/serp-loop.js +80 -0
- package/dist/tm/serp-loop.js.map +1 -0
- package/dist/tm/stats.d.ts +10 -0
- package/dist/tm/stats.d.ts.map +1 -1
- package/dist/tm/stats.js +160 -0
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/webfetch.d.ts +4 -0
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +75 -8
- package/dist/tm/webfetch.js.map +1 -1
- package/docs/installation-v2.md +436 -0
- package/docs/installation.md +12 -0
- package/docs/research/agent-data-exchange.md +117 -0
- package/docs/research/browser-pane.md +89 -0
- package/docs/research/host-http-api.md +132 -0
- package/docs/research/host-subagent-injection.md +187 -0
- package/docs/research/plugin-loader-contract.md +261 -0
- package/docs/research/v2-builtin-tools.md +447 -0
- package/docs/v2-port-notes.md +315 -0
- package/index.js +7 -0
- package/package.json +64 -61
- package/scripts/gen-v2-config.mjs +304 -0
- package/scripts/lib/config-surgery.cjs +239 -0
- package/scripts/lib/fake-ctx.mjs +146 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The v2 permission-guard layer (#7).
|
|
3
|
+
*
|
|
4
|
+
* v1 enforced two things in code that v2 can enforce one level lower — at the
|
|
5
|
+
* host's own `permission.hook("evaluate")`, whose `effect` is mutable and whose
|
|
6
|
+
* `resources` carry the concrete command line or URL (live-probed: `action`
|
|
7
|
+
* arrives as shell / read / edit / external_directory with the real payload).
|
|
8
|
+
*
|
|
9
|
+
* 1. THE EGRESS RED LINE ON THE HOST'S OWN WEB TOOL. v1 never had to think
|
|
10
|
+
* about this because native `webfetch` was denied in the whitelist; on v2 a
|
|
11
|
+
* Build-class agent has it, and it has no notion of `169.254.169.254` — the
|
|
12
|
+
* cloud metadata endpoint whose response is temporary credentials. The
|
|
13
|
+
* domain allowlist answers "is this host on the list"; only `checkWebUrl`
|
|
14
|
+
* asks the address question, and that question has to be asked of the native
|
|
15
|
+
* path too or the port becomes a way around it.
|
|
16
|
+
* 2. R6's per-command precision. v2's closest thing to v1's pattern-object
|
|
17
|
+
* escalation is currently "ask on every shell command", which is honest but
|
|
18
|
+
* coarse. The classifier can restore the per-command shape — but only if
|
|
19
|
+
* the host actually calls `evaluate` for shell, which has not been proven on
|
|
20
|
+
* a live 2.0.16 yet. So the strictening hook is installed either way and
|
|
21
|
+
* COUNTS what it sees, while the config-level coarse ask stays in force
|
|
22
|
+
* until `TM_R6_FINE_ASK=on` says otherwise. A guard that fails open because
|
|
23
|
+
* we trusted an unproven hook is the opposite of this product.
|
|
24
|
+
*
|
|
25
|
+
* The hook only ever makes a decision STRICTER (allow -> ask, anything -> deny on
|
|
26
|
+
* a red line). It never loosens what the host or the user's own rules chose.
|
|
27
|
+
*/
|
|
28
|
+
import { R2_DANGER_BASH_ASK_PATTERNS, R6_ENV_BASH_ASK_PATTERNS, commandMatchesAnyAskPattern, isEnvFilePath, } from "../envprotect.js";
|
|
29
|
+
import { classifyHost } from "../tm/egress.js";
|
|
30
|
+
const rank = { allow: 0, ask: 1, deny: 2 };
|
|
31
|
+
/** A resource may be a full URL or a bare host; both have to be answered, since
|
|
32
|
+
* the host's own tool is free to hand us either shape. */
|
|
33
|
+
function hostOf(text) {
|
|
34
|
+
const s = text.trim();
|
|
35
|
+
if (!s)
|
|
36
|
+
return null;
|
|
37
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(s).hostname;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// A path-shaped resource is not a host.
|
|
46
|
+
if (s.startsWith("/") || s.includes(" "))
|
|
47
|
+
return null;
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
/** The address question, asked of the host's native web path. */
|
|
51
|
+
export function webGuard(resources) {
|
|
52
|
+
for (const raw of resources) {
|
|
53
|
+
const text = String(raw ?? "");
|
|
54
|
+
if (isEnvFilePath(text)) {
|
|
55
|
+
return { effect: "deny", why: "env-file URL", message: "R6 红线:这个 URL 指向环境文件(.env / shell rc 家族),不授权、不弹窗。" };
|
|
56
|
+
}
|
|
57
|
+
const host = hostOf(text);
|
|
58
|
+
if (!host)
|
|
59
|
+
continue;
|
|
60
|
+
const verdict = classifyHost(host);
|
|
61
|
+
if (verdict.level === "forbidden") {
|
|
62
|
+
return {
|
|
63
|
+
effect: "deny",
|
|
64
|
+
why: `egress:${verdict.via ?? "forbidden"}`,
|
|
65
|
+
message: `${host} 是元数据/链路本地/保留地址(${verdict.via ?? "特殊用途"}),拿它的响应等于把临时凭据读进上下文和轨迹。这条不可授权,配置里的 "*" 也不算授权。`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (verdict.level === "private") {
|
|
69
|
+
return {
|
|
70
|
+
effect: "ask",
|
|
71
|
+
why: `egress-private:${verdict.via ?? "private"}`,
|
|
72
|
+
message: `${host} 是私网/回环地址(${verdict.via ?? "私有"})。v1 在这里要求用户逐次批准;v2 的插件弹不出对话框,所以交给宿主自己的确认——不要重复调用绕过它。`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** R6's env face and R2's danger face, per command line. */
|
|
79
|
+
export function shellGuard(command, mode) {
|
|
80
|
+
const text = String(command ?? "").trim();
|
|
81
|
+
if (!text || mode === "off")
|
|
82
|
+
return null;
|
|
83
|
+
if (commandMatchesAnyAskPattern(text, R2_DANGER_BASH_ASK_PATTERNS)) {
|
|
84
|
+
return { effect: "ask", why: "r2-danger", message: "这条命令落在 R2 危险面上(删除/推送/发布/杀进程一类),要人批准一次。" };
|
|
85
|
+
}
|
|
86
|
+
if (commandMatchesAnyAskPattern(text, R6_ENV_BASH_ASK_PATTERNS)) {
|
|
87
|
+
return { effect: "ask", why: "r6-env", message: "这条命令读环境变量(R6),要人批准一次。" };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every sub-agent dispatch runs in the background.
|
|
93
|
+
*
|
|
94
|
+
* The user's standing instruction (2026-09-25). It is not a preference tweak: a
|
|
95
|
+
* foreground `subagent` call blocks the lead for the whole child run, which is
|
|
96
|
+
* the one thing the throughput mandate cannot survive — and on v2 background is
|
|
97
|
+
* a first-class host capability (`subagent {background:true}` returns
|
|
98
|
+
* immediately and the parent is notified when the child finishes), so nothing
|
|
99
|
+
* has to be opted into at the environment level the way v1 needed
|
|
100
|
+
* `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS`.
|
|
101
|
+
*
|
|
102
|
+
* `execute.before` takes a mutable `input` (docs say "inspect or REPLACE"; a
|
|
103
|
+
* live host confirmed the write lands), so this is the honest place to enforce
|
|
104
|
+
* it — a prompt rule thousands of tokens earlier is not the same thing. An
|
|
105
|
+
* input that is not an object is left alone rather than invented.
|
|
106
|
+
*/
|
|
107
|
+
export async function applyV2BackgroundForce(ctx, opts = {}) {
|
|
108
|
+
const report = { seen: 0, forced: 0 };
|
|
109
|
+
const tool = ctx.tool;
|
|
110
|
+
if (!tool || typeof tool.hook !== "function")
|
|
111
|
+
return { registrations: [], report };
|
|
112
|
+
const registrations = [
|
|
113
|
+
await tool.hook("execute.before", (event) => {
|
|
114
|
+
if (String(event?.tool ?? "") !== "subagent")
|
|
115
|
+
return;
|
|
116
|
+
// #22: rewriting the input of a call that is not ours is the clearest kind
|
|
117
|
+
// of side effect — a user who chose a synchronous dispatch in build mode
|
|
118
|
+
// would have it taken away by a plugin installed for Team.
|
|
119
|
+
if (opts.scope && opts.scope.count(opts.scope.decide(event)) !== "ours")
|
|
120
|
+
return;
|
|
121
|
+
const input = event.input;
|
|
122
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
123
|
+
return;
|
|
124
|
+
report.seen++;
|
|
125
|
+
if (input.background === true)
|
|
126
|
+
return;
|
|
127
|
+
input.background = true;
|
|
128
|
+
report.forced++;
|
|
129
|
+
}),
|
|
130
|
+
];
|
|
131
|
+
return { registrations, report };
|
|
132
|
+
}
|
|
133
|
+
export async function applyV2PermissionGuards(ctx, opts) {
|
|
134
|
+
const report = { seen: 0, byAction: {}, strictened: 0, denied: 0, shellMatched: 0, foreignSkipped: 0 };
|
|
135
|
+
const permission = ctx.permission;
|
|
136
|
+
if (!permission || typeof permission.hook !== "function") {
|
|
137
|
+
return { registrations: [], report, installed: false };
|
|
138
|
+
}
|
|
139
|
+
const registrations = [
|
|
140
|
+
await permission.hook("evaluate", (event) => {
|
|
141
|
+
const action = String(event?.action ?? "");
|
|
142
|
+
const resources = (Array.isArray(event?.resources) ? event.resources : []).map(String);
|
|
143
|
+
report.seen++;
|
|
144
|
+
report.byAction[action] = (report.byAction[action] ?? 0) + 1;
|
|
145
|
+
// #22: the host evaluates permissions for every agent. Tightening a rule a
|
|
146
|
+
// build/plan session is running under is a change to somebody else's mode,
|
|
147
|
+
// so inside Team only — and the count says how much of that floor is
|
|
148
|
+
// therefore NOT ours to enforce (see the note in the boot line).
|
|
149
|
+
if (opts.scope && opts.scope.count(opts.scope.decide(event)) !== "ours") {
|
|
150
|
+
report.foreignSkipped++;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const decision = action === "webfetch" || action === "websearch"
|
|
154
|
+
? webGuard(resources)
|
|
155
|
+
: action === "shell"
|
|
156
|
+
? shellGuard(resources[0], opts.envProtectMode)
|
|
157
|
+
: null;
|
|
158
|
+
if (!decision)
|
|
159
|
+
return;
|
|
160
|
+
if (action === "shell")
|
|
161
|
+
report.shellMatched++;
|
|
162
|
+
// Only ever stricter. A host that already decided to ask or deny keeps
|
|
163
|
+
// that decision — our classifier is a floor, not an override of the user.
|
|
164
|
+
if (rank[decision.effect] <= rank[event.effect ?? "allow"])
|
|
165
|
+
return;
|
|
166
|
+
event.effect = decision.effect;
|
|
167
|
+
if (decision.message)
|
|
168
|
+
event.message = decision.message;
|
|
169
|
+
if (decision.effect === "deny")
|
|
170
|
+
report.denied++;
|
|
171
|
+
else
|
|
172
|
+
report.strictened++;
|
|
173
|
+
}),
|
|
174
|
+
];
|
|
175
|
+
return { registrations, report, installed: true };
|
|
176
|
+
}
|
|
177
|
+
/** Does the config still need the coarse `shell -> ask` escalation? Only while
|
|
178
|
+
* the fine path is switched off; with the classifier in charge, escalating every
|
|
179
|
+
* command would mask it.
|
|
180
|
+
* Was the coarse default until a live host proved the seam exists: a
|
|
181
|
+
* `--standalone` run of the v2 personality recorded
|
|
182
|
+
* `{action:"shell", resourceCount:1, hasUrl:false}` reaching `evaluate` for a real
|
|
183
|
+
* `git status --short` (2026-09-25), which is the observation this function was
|
|
184
|
+
* waiting for. So the classifier is in charge by default and the escalation is
|
|
185
|
+
* the fallback again — `TM_R6_FINE_ASK=off` restores "every command asks", which is
|
|
186
|
+
* what a user should reach for if their host build turns out not to fire the hook.
|
|
187
|
+
* An absent hook still forces coarse: no seam, no per-command judgement.
|
|
188
|
+
*/
|
|
189
|
+
export function needsCoarseShellAsk(env, guardsInstalled) {
|
|
190
|
+
if (!guardsInstalled)
|
|
191
|
+
return true;
|
|
192
|
+
const v = String(env.TM_R6_FINE_ASK ?? "").trim();
|
|
193
|
+
// Only an EXPLICIT off falls back; an empty or unparseable value keeps the
|
|
194
|
+
// classifier, because "unset" is now the supported configuration.
|
|
195
|
+
if (/^(0|false|no|off)$/i.test(v))
|
|
196
|
+
return true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=v2-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2-guard.js","sourceRoot":"","sources":["../../src/host/v2-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { V2Registration } from "./v2-types.js";
|
|
2
|
+
import type { TeamScope } from "./v2-scope.js";
|
|
3
|
+
import { detectContentType } from "../tm/preview.js";
|
|
4
|
+
/**
|
|
5
|
+
* JIT context governance over the HOST's own tools.
|
|
6
|
+
*
|
|
7
|
+
* The whole product rests on one promise: an oversized tool result never enters
|
|
8
|
+
* the context window — it lands in the run store and the model gets an
|
|
9
|
+
* ≤80-token preview plus a handle. That promise was true only inside our own
|
|
10
|
+
* `tm_*` tools, which is what made `tm_read`/`tm_grep`/`tm_bash` load-bearing
|
|
11
|
+
* rather than merely nicer. The moment a role is allowed the native `read` or
|
|
12
|
+
* `shell` (which is the retirement plan for those three), an un-governed native
|
|
13
|
+
* result would put the promise back to zero for exactly the tools a user would
|
|
14
|
+
* most plausibly enable — and the promise would then depend on which tool the
|
|
15
|
+
* model happened to pick, which is not a guarantee at all.
|
|
16
|
+
*
|
|
17
|
+
* So the governance moves to the seam that does not care what the model chose.
|
|
18
|
+
* `tool.execute.after` hands us the finished result for EVERY tool (measured
|
|
19
|
+
* live: `{action:"shell"}` reaches `permission.evaluate`, and the same call
|
|
20
|
+
* reaches `execute.after` with `resultKeys: content / metadata / output`), and
|
|
21
|
+
* the host documents `result` as mutable. One hook, both worlds.
|
|
22
|
+
*
|
|
23
|
+
* Design constraints this file is written under:
|
|
24
|
+
* - **It only ever shrinks what reaches the context, never what the tool did.**
|
|
25
|
+
* The bytes on disk are the host's; a preview plus a handle replaces the text
|
|
26
|
+
* part. `metadata` and any non-text part (an image attachment) pass through
|
|
27
|
+
* untouched — dropping a screenshot to save tokens would be a bad trade.
|
|
28
|
+
* - **It reuses the same threshold, preview builder, store and HMAC handles as
|
|
29
|
+
* `tm_*`**, because a second implementation would drift, and the drift would
|
|
30
|
+
* show up as "the same 40 KB was free through one tool and offloaded through
|
|
31
|
+
* another".
|
|
32
|
+
* - **A native result is rendered as native-shaped text.** `tm_*` returns our own
|
|
33
|
+
* structured object; overwriting a host tool's text part with `{offloaded:true,
|
|
34
|
+
* ref:…}` would hand the model a shape it has no reason to understand. The
|
|
35
|
+
* sentence below says the same facts in prose the tm_* tools already train it
|
|
36
|
+
* to act on: how much was saved, the handle, how to page it in.
|
|
37
|
+
* - Nothing here may throw into somebody's tool call: a governance failure
|
|
38
|
+
* degrades to the host's verbatim result, and says so in the trajectory.
|
|
39
|
+
*/
|
|
40
|
+
/** Tools whose NATIVE output we govern. Deliberately a closed list: the built-in
|
|
41
|
+
* agent/patch/question results have shapes nobody has observed here, and rewriting
|
|
42
|
+
* a shape you guessed at is content destruction, not governance.
|
|
43
|
+
*
|
|
44
|
+
* `execute` is on the list for one specific reason, and it is the reason the
|
|
45
|
+
* native browser can be governed at all: the Team's direct tool surface on v2 is
|
|
46
|
+
* six tools (edit / execute / question / shell / subagent / write), so
|
|
47
|
+
* `tools.browser.*` -- every snapshot, tab list and evaluate result -- reaches the
|
|
48
|
+
* context window ONLY as the aggregate return of one Code Mode program. There is
|
|
49
|
+
* no other door, so governing `execute` IS "put JIT on the native browser". */
|
|
50
|
+
export declare const NATIVE_GOVERNED_TOOLS: ReadonlySet<string>;
|
|
51
|
+
/** Is this tool's output inside the governed surface? The host's browser catalog is
|
|
52
|
+
* 45 tools sharing one namespace (`browser_*`, each registered with
|
|
53
|
+
* `permission:"browser"`), and a snapshot from any of them is exactly the oversized
|
|
54
|
+
* payload the offload promise is about — enumerating 45 names would only guarantee
|
|
55
|
+
* that a host adding the 46th escapes us. Unknown RESULT shapes are still left
|
|
56
|
+
* untouched; that test is per payload, not per name. */
|
|
57
|
+
export declare function isGovernedNative(tool: string): boolean;
|
|
58
|
+
export interface V2OffloadReport {
|
|
59
|
+
seen: number;
|
|
60
|
+
/** every execute.after we resolved to a Team session, governed tool or not — the
|
|
61
|
+
* denominator the coverage sentence is read against (#23: "most tool calls are
|
|
62
|
+
* JIT-governed" has to be a number, not a feeling) */
|
|
63
|
+
ours: number;
|
|
64
|
+
/** ours, for a tool whose output we do not touch (unknown tool, or a name the
|
|
65
|
+
* governed set has not been widened to) — counted rather than invisible */
|
|
66
|
+
unmatched: number;
|
|
67
|
+
considered: number;
|
|
68
|
+
offloaded: number;
|
|
69
|
+
/** addressing results (native browser snapshots) that stayed IN context, capped
|
|
70
|
+
* rather than replaced by a handle — the tokens they cost are counted, and so is
|
|
71
|
+
* the fact that we chose a usable `ref` over the smaller number */
|
|
72
|
+
capped: number;
|
|
73
|
+
/** report-shaped payloads (tables kept, prose dropped) — a third outcome, because
|
|
74
|
+
* "offloaded" and "capped" answer different questions about what entered context */
|
|
75
|
+
reportCapped: number;
|
|
76
|
+
/** envelopes RECOGNISED, whether or not they needed rewriting — a counter that
|
|
77
|
+
* moves only on a rewrite cannot tell "the channel is alive, nothing was big"
|
|
78
|
+
* apart from "this host's format is not the one we match" (which is exactly how
|
|
79
|
+
* the v1 `<task id=` matcher read as zero on v2). */
|
|
80
|
+
envelopes: number;
|
|
81
|
+
degraded: number;
|
|
82
|
+
tokensSaved: number;
|
|
83
|
+
byTool: Record<string, {
|
|
84
|
+
seen: number;
|
|
85
|
+
offloaded: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
interface OffloadDeps {
|
|
89
|
+
/** the shared tm pipeline instance — one store, one step counter, one threshold table */
|
|
90
|
+
pipelines: {
|
|
91
|
+
nextStepId: () => string;
|
|
92
|
+
govern: (stepId: string, tool: string, content: string, opts: {
|
|
93
|
+
contentType: ReturnType<typeof detectContentType>;
|
|
94
|
+
clue?: string;
|
|
95
|
+
}) => unknown;
|
|
96
|
+
};
|
|
97
|
+
env?: Record<string, string | undefined>;
|
|
98
|
+
/** Team-scope isolation (#22): a non-Team session's result is nobody's to rewrite
|
|
99
|
+
* but the host's, and an unknown owner is treated as not-ours and COUNTED — a
|
|
100
|
+
* governance layer that silently stopped applying is the same overstated claim
|
|
101
|
+
* this product exists to refuse. */
|
|
102
|
+
scope?: TeamScope;
|
|
103
|
+
}
|
|
104
|
+
/** Rendered in the user's tool-output language (the tm_* strings are Chinese and
|
|
105
|
+
* flow into every reply an agent writes about them). */
|
|
106
|
+
/**
|
|
107
|
+
* Tools whose result IS an addressing table: native `browser_click` / `browser_fill`
|
|
108
|
+
* take a `ref` out of the snapshot, so a preview that drops the ref tokens does not
|
|
109
|
+
* save the model a round-trip — it makes it click the wrong element, or spend a call
|
|
110
|
+
* re-snapshotting. For these two the text stays in context, capped, with the handle
|
|
111
|
+
* kept for the tail. 1 200 tokens is the budget tm_browser's own snapshot already
|
|
112
|
+
* runs at, so the governed door and the native one cost the same window space.
|
|
113
|
+
*/
|
|
114
|
+
export declare const ADDRESSING_NATIVE_TOOLS: ReadonlySet<string>;
|
|
115
|
+
export declare const NATIVE_SNAPSHOT_MAX_TOKENS = 1200;
|
|
116
|
+
export declare function snapshotTokenBudget(env: Record<string, string | undefined> | undefined): number;
|
|
117
|
+
export declare function renderAddressingCap(tool: string, capped: string, governed: {
|
|
118
|
+
ref: string;
|
|
119
|
+
access_token: string;
|
|
120
|
+
expire_at: number;
|
|
121
|
+
tokens: number;
|
|
122
|
+
}): string;
|
|
123
|
+
/** The budget a report (a `tm_stats`-shaped answer, a `tm_join` summary) keeps in
|
|
124
|
+
* context: tables win the space, prose is what gets dropped. Pinned separately from
|
|
125
|
+
* the snapshot budget because a report has more prose per table. */
|
|
126
|
+
export declare const NATIVE_REPORT_MAX_TOKENS = 1600;
|
|
127
|
+
export declare function reportTokenBudget(env: Record<string, string | undefined> | undefined): number;
|
|
128
|
+
export declare function renderReportCap(tool: string, capped: string, governed: {
|
|
129
|
+
ref: string;
|
|
130
|
+
access_token: string;
|
|
131
|
+
expire_at: number;
|
|
132
|
+
tokens: number;
|
|
133
|
+
}, tablesDropped: number): string;
|
|
134
|
+
export declare function renderNativeOffload(tool: string, governed: {
|
|
135
|
+
offloaded: true;
|
|
136
|
+
ref: string;
|
|
137
|
+
access_token: string;
|
|
138
|
+
expire_at: number;
|
|
139
|
+
tokens: number;
|
|
140
|
+
preview: string;
|
|
141
|
+
}): string;
|
|
142
|
+
export declare function applyV2NativeOffload(ctx: unknown, deps: OffloadDeps): {
|
|
143
|
+
registrations: Promise<V2Registration>[];
|
|
144
|
+
report: V2OffloadReport;
|
|
145
|
+
active: boolean;
|
|
146
|
+
};
|
|
147
|
+
export {};
|
|
148
|
+
//# sourceMappingURL=v2-offload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2-offload.d.ts","sourceRoot":"","sources":["../../src/host/v2-offload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { capKeepingAddressing, capKeepingTables, detectContentType, hasMarkdownTable } from "../tm/preview.js";
|
|
2
|
+
import { parseHostEnvelope, renderOffloadedSubagent } from "../task-offload.js";
|
|
3
|
+
import { estimateTokens } from "../tm/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* JIT context governance over the HOST's own tools.
|
|
6
|
+
*
|
|
7
|
+
* The whole product rests on one promise: an oversized tool result never enters
|
|
8
|
+
* the context window — it lands in the run store and the model gets an
|
|
9
|
+
* ≤80-token preview plus a handle. That promise was true only inside our own
|
|
10
|
+
* `tm_*` tools, which is what made `tm_read`/`tm_grep`/`tm_bash` load-bearing
|
|
11
|
+
* rather than merely nicer. The moment a role is allowed the native `read` or
|
|
12
|
+
* `shell` (which is the retirement plan for those three), an un-governed native
|
|
13
|
+
* result would put the promise back to zero for exactly the tools a user would
|
|
14
|
+
* most plausibly enable — and the promise would then depend on which tool the
|
|
15
|
+
* model happened to pick, which is not a guarantee at all.
|
|
16
|
+
*
|
|
17
|
+
* So the governance moves to the seam that does not care what the model chose.
|
|
18
|
+
* `tool.execute.after` hands us the finished result for EVERY tool (measured
|
|
19
|
+
* live: `{action:"shell"}` reaches `permission.evaluate`, and the same call
|
|
20
|
+
* reaches `execute.after` with `resultKeys: content / metadata / output`), and
|
|
21
|
+
* the host documents `result` as mutable. One hook, both worlds.
|
|
22
|
+
*
|
|
23
|
+
* Design constraints this file is written under:
|
|
24
|
+
* - **It only ever shrinks what reaches the context, never what the tool did.**
|
|
25
|
+
* The bytes on disk are the host's; a preview plus a handle replaces the text
|
|
26
|
+
* part. `metadata` and any non-text part (an image attachment) pass through
|
|
27
|
+
* untouched — dropping a screenshot to save tokens would be a bad trade.
|
|
28
|
+
* - **It reuses the same threshold, preview builder, store and HMAC handles as
|
|
29
|
+
* `tm_*`**, because a second implementation would drift, and the drift would
|
|
30
|
+
* show up as "the same 40 KB was free through one tool and offloaded through
|
|
31
|
+
* another".
|
|
32
|
+
* - **A native result is rendered as native-shaped text.** `tm_*` returns our own
|
|
33
|
+
* structured object; overwriting a host tool's text part with `{offloaded:true,
|
|
34
|
+
* ref:…}` would hand the model a shape it has no reason to understand. The
|
|
35
|
+
* sentence below says the same facts in prose the tm_* tools already train it
|
|
36
|
+
* to act on: how much was saved, the handle, how to page it in.
|
|
37
|
+
* - Nothing here may throw into somebody's tool call: a governance failure
|
|
38
|
+
* degrades to the host's verbatim result, and says so in the trajectory.
|
|
39
|
+
*/
|
|
40
|
+
/** Tools whose NATIVE output we govern. Deliberately a closed list: the built-in
|
|
41
|
+
* agent/patch/question results have shapes nobody has observed here, and rewriting
|
|
42
|
+
* a shape you guessed at is content destruction, not governance.
|
|
43
|
+
*
|
|
44
|
+
* `execute` is on the list for one specific reason, and it is the reason the
|
|
45
|
+
* native browser can be governed at all: the Team's direct tool surface on v2 is
|
|
46
|
+
* six tools (edit / execute / question / shell / subagent / write), so
|
|
47
|
+
* `tools.browser.*` -- every snapshot, tab list and evaluate result -- reaches the
|
|
48
|
+
* context window ONLY as the aggregate return of one Code Mode program. There is
|
|
49
|
+
* no other door, so governing `execute` IS "put JIT on the native browser". */
|
|
50
|
+
export const NATIVE_GOVERNED_TOOLS = new Set([
|
|
51
|
+
"read",
|
|
52
|
+
"grep",
|
|
53
|
+
"glob",
|
|
54
|
+
"shell",
|
|
55
|
+
"bash",
|
|
56
|
+
"webfetch",
|
|
57
|
+
"websearch",
|
|
58
|
+
"execute",
|
|
59
|
+
"edit",
|
|
60
|
+
"write",
|
|
61
|
+
"patch",
|
|
62
|
+
"question",
|
|
63
|
+
/*
|
|
64
|
+
* `subagent` is v2's version of Plan B, and it is here because of what was
|
|
65
|
+
* MEASURED rather than what v1 did. A live run recorded
|
|
66
|
+
* `execute.before subagent {agent,background,description,prompt}` ->
|
|
67
|
+
* `permission.evaluate action=subagent` -> `execute.after subagent
|
|
68
|
+
* {content,metadata,output}`: a synchronous child's whole reply arrives as THIS
|
|
69
|
+
* tool's result, so one hook governs it. The same run showed
|
|
70
|
+
* `session.hook("context")` carrying a single message and no
|
|
71
|
+
* `<task id=… state="completed">` envelope at all -- so v1's chat.message offload
|
|
72
|
+
* has no anchor on v2, and rewriting the message list against a guessed shape is
|
|
73
|
+
* how evidence gets deleted silently (the same reason host-hooks.ts declines
|
|
74
|
+
* `experimental.chat.messages.transform`). Consequence stated plainly: a
|
|
75
|
+
* BACKGROUND child's reply, which the host injects later, is NOT governed here --
|
|
76
|
+
* closing that needs the ctx.event/ctx.session rebuild, which is an open decision.
|
|
77
|
+
*/
|
|
78
|
+
"subagent",
|
|
79
|
+
]);
|
|
80
|
+
/** Is this tool's output inside the governed surface? The host's browser catalog is
|
|
81
|
+
* 45 tools sharing one namespace (`browser_*`, each registered with
|
|
82
|
+
* `permission:"browser"`), and a snapshot from any of them is exactly the oversized
|
|
83
|
+
* payload the offload promise is about — enumerating 45 names would only guarantee
|
|
84
|
+
* that a host adding the 46th escapes us. Unknown RESULT shapes are still left
|
|
85
|
+
* untouched; that test is per payload, not per name. */
|
|
86
|
+
export function isGovernedNative(tool) {
|
|
87
|
+
return NATIVE_GOVERNED_TOOLS.has(tool) || tool.startsWith("browser_");
|
|
88
|
+
}
|
|
89
|
+
const textPart = (p) => !!p && typeof p === "object" && p.type === "text" && typeof p.text === "string";
|
|
90
|
+
/** The host's result shape, as measured: `{content, metadata, output}` with
|
|
91
|
+
* `content` an array of parts. Every other shape is left alone rather than
|
|
92
|
+
* interpreted — an unknown shape we rewrite is content destruction. */
|
|
93
|
+
function locatableText(result) {
|
|
94
|
+
const r = result;
|
|
95
|
+
if (!r || !Array.isArray(r.content))
|
|
96
|
+
return null;
|
|
97
|
+
let joined = "";
|
|
98
|
+
let index = -1;
|
|
99
|
+
for (let i = 0; i < r.content.length; i++) {
|
|
100
|
+
const p = r.content[i];
|
|
101
|
+
if (!textPart(p))
|
|
102
|
+
continue;
|
|
103
|
+
if (index < 0)
|
|
104
|
+
index = i;
|
|
105
|
+
joined += (index === i && joined === "" ? "" : joined ? "\n" : "") + p.text;
|
|
106
|
+
}
|
|
107
|
+
if (index < 0 || !joined)
|
|
108
|
+
return null;
|
|
109
|
+
return { parts: r.content, index, text: joined };
|
|
110
|
+
}
|
|
111
|
+
/** Rendered in the user's tool-output language (the tm_* strings are Chinese and
|
|
112
|
+
* flow into every reply an agent writes about them). */
|
|
113
|
+
/**
|
|
114
|
+
* Tools whose result IS an addressing table: native `browser_click` / `browser_fill`
|
|
115
|
+
* take a `ref` out of the snapshot, so a preview that drops the ref tokens does not
|
|
116
|
+
* save the model a round-trip — it makes it click the wrong element, or spend a call
|
|
117
|
+
* re-snapshotting. For these two the text stays in context, capped, with the handle
|
|
118
|
+
* kept for the tail. 1 200 tokens is the budget tm_browser's own snapshot already
|
|
119
|
+
* runs at, so the governed door and the native one cost the same window space.
|
|
120
|
+
*/
|
|
121
|
+
export const ADDRESSING_NATIVE_TOOLS = new Set(["browser_snapshot", "browser_find"]);
|
|
122
|
+
export const NATIVE_SNAPSHOT_MAX_TOKENS = 1200;
|
|
123
|
+
export function snapshotTokenBudget(env) {
|
|
124
|
+
const raw = Number(String(env?.TM_NATIVE_SNAPSHOT_MAX_TOKENS ?? "").trim());
|
|
125
|
+
return Number.isFinite(raw) && raw >= 100 ? Math.min(20_000, Math.floor(raw)) : NATIVE_SNAPSHOT_MAX_TOKENS;
|
|
126
|
+
}
|
|
127
|
+
export function renderAddressingCap(tool, capped, governed) {
|
|
128
|
+
const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
|
|
129
|
+
return (capped +
|
|
130
|
+
`
|
|
131
|
+
|
|
132
|
+
(原生 ${tool} 的快照按 JIT 治理截到预算内,没有整体卸载:截断部分里的 ref 不在你眼前,` +
|
|
133
|
+
`需要时用 tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"lines" } 分段取回再点,` +
|
|
134
|
+
`不要为了看一眼把全文读回来。原文 ${governed.tokens} token · 过期 ${governed.expire_at} · ${when})`);
|
|
135
|
+
}
|
|
136
|
+
/** The budget a report (a `tm_stats`-shaped answer, a `tm_join` summary) keeps in
|
|
137
|
+
* context: tables win the space, prose is what gets dropped. Pinned separately from
|
|
138
|
+
* the snapshot budget because a report has more prose per table. */
|
|
139
|
+
export const NATIVE_REPORT_MAX_TOKENS = 1600;
|
|
140
|
+
export function reportTokenBudget(env) {
|
|
141
|
+
const raw = Number(String(env?.TM_NATIVE_REPORT_MAX_TOKENS ?? "").trim());
|
|
142
|
+
return Number.isFinite(raw) && raw >= 200 ? Math.min(20_000, Math.floor(raw)) : NATIVE_REPORT_MAX_TOKENS;
|
|
143
|
+
}
|
|
144
|
+
export function renderReportCap(tool, capped, governed, tablesDropped) {
|
|
145
|
+
const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
|
|
146
|
+
return (capped +
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
(原生 ${tool} 的返回是报告形的:表格已整份留在上面,被删掉的只有表格之间的说明文字——` +
|
|
150
|
+
`全文 ${governed.tokens} token,别为了看一句散文把整份读回来。` +
|
|
151
|
+
(tablesDropped > 0 ? `注意:预算之外还有 ${tablesDropped} 行表格内容没进来,需要时用 ` : "需要更多行时用 ") +
|
|
152
|
+
`tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"lines" } 分段取。过期 ${governed.expire_at} · ${when})`);
|
|
153
|
+
}
|
|
154
|
+
export function renderNativeOffload(tool, governed) {
|
|
155
|
+
const when = new Date(governed.expire_at).toISOString().replace("T", " ").slice(0, 16);
|
|
156
|
+
return [
|
|
157
|
+
`(原生 ${tool} 的输出过大,已按 JIT 治理卸载:${governed.tokens} token 没有进入上下文——全文在句柄里,不在下面这段里。)`,
|
|
158
|
+
"",
|
|
159
|
+
governed.preview,
|
|
160
|
+
"",
|
|
161
|
+
`取全文:tm_fetch { ref:"${governed.ref}", access_token:"${governed.access_token}", mode:"structure" | "lines" }(过期 ${governed.expire_at} · ${when})`,
|
|
162
|
+
"只要摘要就到此为止;需要具体行请用 mode:\"lines\" 分段取,不要为了看一眼把全文读回来。",
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
165
|
+
export function applyV2NativeOffload(ctx, deps) {
|
|
166
|
+
const env = deps.env ?? process.env;
|
|
167
|
+
const off = /^(0|false|no|off)$/i.test(String(env.TM_NATIVE_OFFLOAD ?? "").trim());
|
|
168
|
+
const report = { seen: 0, ours: 0, unmatched: 0, considered: 0, offloaded: 0, capped: 0, reportCapped: 0, envelopes: 0, degraded: 0, tokensSaved: 0, byTool: {} };
|
|
169
|
+
const hook = ctx?.tool?.hook;
|
|
170
|
+
if (typeof hook !== "function") {
|
|
171
|
+
// No seam at all: report it, do not silently pretend the promise holds.
|
|
172
|
+
return { registrations: [], report, active: false };
|
|
173
|
+
}
|
|
174
|
+
const reg = hook("execute.after", (raw) => {
|
|
175
|
+
const event = raw;
|
|
176
|
+
const tool = String(event?.tool ?? "");
|
|
177
|
+
// The owner question comes FIRST: a foreign or unattributable session is left
|
|
178
|
+
// alone no matter which tool it called, and the counters then describe every
|
|
179
|
+
// call we looked at rather than only the ones we recognised a tool for.
|
|
180
|
+
if (deps.scope) {
|
|
181
|
+
if (deps.scope.count(deps.scope.decide(event)) !== "ours")
|
|
182
|
+
return;
|
|
183
|
+
// A resolved call is also a fact about the session — remember it so a later
|
|
184
|
+
// event that arrives without `agent` still resolves to ours.
|
|
185
|
+
deps.scope.learn(event.agent, event.sessionID);
|
|
186
|
+
}
|
|
187
|
+
report.ours++;
|
|
188
|
+
if (!isGovernedNative(tool)) {
|
|
189
|
+
report.unmatched++;
|
|
190
|
+
report.byTool[tool] ??= { seen: 0, offloaded: 0 };
|
|
191
|
+
report.byTool[tool].seen++;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
report.seen++;
|
|
195
|
+
report.byTool[tool] ??= { seen: 0, offloaded: 0 };
|
|
196
|
+
report.byTool[tool].seen++;
|
|
197
|
+
if (off)
|
|
198
|
+
return;
|
|
199
|
+
const found = locatableText(event?.result);
|
|
200
|
+
if (!found)
|
|
201
|
+
return;
|
|
202
|
+
report.considered++;
|
|
203
|
+
// A sub-agent reply arrives wrapped in the host's own envelope. Offloading it
|
|
204
|
+
// means replacing the BODY and reproducing the wrapper: the host and the UI key
|
|
205
|
+
// on `<subagent sessionID=…>`, so flattening it into a generic governed result
|
|
206
|
+
// would delete the very pointer the lead needs to fetch the whole thing back.
|
|
207
|
+
const envelope = tool === "subagent" ? parseHostEnvelope(found.text) : null;
|
|
208
|
+
if (envelope)
|
|
209
|
+
report.envelopes++;
|
|
210
|
+
const stepId = deps.pipelines.nextStepId();
|
|
211
|
+
// Content class is inferred from the BODY, not from a path we do not have:
|
|
212
|
+
// that is the same basis tm_bash uses, so a 30 KB JSON stdout gets the data
|
|
213
|
+
// tier here and there too.
|
|
214
|
+
const contentType = detectContentType(found.text);
|
|
215
|
+
let governed;
|
|
216
|
+
try {
|
|
217
|
+
governed = deps.pipelines.govern(stepId, `native:${tool}`, found.text, {
|
|
218
|
+
contentType,
|
|
219
|
+
clue: `原生 ${tool} 的结果`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
report.degraded++;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!governed || typeof governed !== "object" || governed.offloaded !== true)
|
|
227
|
+
return;
|
|
228
|
+
const g = governed;
|
|
229
|
+
const parts = found.parts;
|
|
230
|
+
if (ADDRESSING_NATIVE_TOOLS.has(tool)) {
|
|
231
|
+
// The snapshot is an addressing table, not a document: replacing it with a
|
|
232
|
+
// handle would strand the next click. Cap it, keep the handle for the tail,
|
|
233
|
+
// and count the tokens that DID enter.
|
|
234
|
+
const budget = snapshotTokenBudget(deps.env);
|
|
235
|
+
const cut = capKeepingAddressing(found.text, budget);
|
|
236
|
+
const capped = `${cut.text}${cut.fellBack
|
|
237
|
+
? `\n\n(这个结果是一整行没有换行的 JSON,按行保留寻址行的做法装不下任何内容,所以退成了从头截断 —— 内容不完整,别把它当成页面是空的;要精确字段就用 tm_fetch mode:"lines" 或改拍一次范围更小的快照。)`
|
|
238
|
+
: cut.addressesDropped > 0
|
|
239
|
+
? `
|
|
240
|
+
|
|
241
|
+
(预算 ${budget} token 之内只装下了部分寻址行:另有 ${cut.addressesDropped} 行带 ref 的内容被截掉了,要点对它们先用 tm_fetch mode:"lines" 取回,或缩小范围重新拍一次快照——不要凭猜去点。)`
|
|
242
|
+
: ""}`;
|
|
243
|
+
const rendered = renderAddressingCap(tool, capped, g);
|
|
244
|
+
parts[found.index] = { type: "text", text: rendered };
|
|
245
|
+
for (let i = found.index + 1; i < parts.length; i++) {
|
|
246
|
+
if (textPart(parts[i]))
|
|
247
|
+
parts[i] = { type: "text", text: "" };
|
|
248
|
+
}
|
|
249
|
+
report.capped++;
|
|
250
|
+
report.tokensSaved += Math.max(0, estimateTokens(found.text) - estimateTokens(rendered));
|
|
251
|
+
report.byTool[tool].offloaded++;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!envelope && hasMarkdownTable(found.text)) {
|
|
255
|
+
// A report is not a log. Offloading `tm_stats` down to an 80-token preview
|
|
256
|
+
// (measured live, on a report that came back through Code Mode's `execute` at
|
|
257
|
+
// 2 917 tokens) leaves the lead describing a table it cannot see and the user
|
|
258
|
+
// reading prose about numbers instead of the numbers. So the tables stay, the
|
|
259
|
+
// prose goes, and the handle still carries the whole thing.
|
|
260
|
+
const budget = reportTokenBudget(deps.env);
|
|
261
|
+
const cut = capKeepingTables(found.text, budget);
|
|
262
|
+
const rendered = renderReportCap(tool, cut.text, g, cut.tablesDropped);
|
|
263
|
+
parts[found.index] = { type: "text", text: rendered };
|
|
264
|
+
for (let i = found.index + 1; i < parts.length; i++) {
|
|
265
|
+
if (textPart(parts[i]))
|
|
266
|
+
parts[i] = { type: "text", text: "" };
|
|
267
|
+
}
|
|
268
|
+
report.reportCapped++;
|
|
269
|
+
report.tokensSaved += Math.max(0, estimateTokens(found.text) - estimateTokens(rendered));
|
|
270
|
+
report.byTool[tool].offloaded++;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const keepTokens = estimateTokens(g.preview);
|
|
274
|
+
parts[found.index] = {
|
|
275
|
+
type: "text",
|
|
276
|
+
text: envelope ? renderOffloadedSubagent(envelope, g.preview, g.tokens) : renderNativeOffload(tool, g),
|
|
277
|
+
};
|
|
278
|
+
for (let i = found.index + 1; i < parts.length; i++) {
|
|
279
|
+
if (textPart(parts[i]))
|
|
280
|
+
parts[i] = { type: "text", text: "" };
|
|
281
|
+
}
|
|
282
|
+
report.offloaded++;
|
|
283
|
+
report.tokensSaved += Math.max(0, estimateTokens(found.text) - keepTokens);
|
|
284
|
+
report.byTool[tool].offloaded++;
|
|
285
|
+
});
|
|
286
|
+
return { registrations: [reg], report, active: !off };
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=v2-offload.js.map
|