@aexol/spectral 0.9.209 → 0.9.211
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/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +13 -0
- package/dist/extensions/browser/tools/console.d.ts.map +1 -1
- package/dist/extensions/browser/tools/console.js +22 -2
- package/dist/extensions/browser/tools/evaluate.d.ts.map +1 -1
- package/dist/extensions/browser/tools/evaluate.js +28 -4
- package/dist/extensions/browser/tools/network.d.ts.map +1 -1
- package/dist/extensions/browser/tools/network.js +22 -2
- package/dist/extensions/seo/tools/fetch.d.ts.map +1 -1
- package/dist/extensions/seo/tools/fetch.js +59 -8
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -1
- package/dist/extensions/web/tools/fetch.js +28 -15
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +53 -2
- package/dist/relay/history-projection.d.ts.map +1 -1
- package/dist/relay/history-projection.js +7 -0
- package/dist/sdk/coding-agent/core/tools/offload.d.ts +102 -0
- package/dist/sdk/coding-agent/core/tools/offload.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/offload.js +207 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +21 -0
- package/dist/server/offload-gc.d.ts +21 -0
- package/dist/server/offload-gc.d.ts.map +1 -0
- package/dist/server/offload-gc.js +111 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAwBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAYlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAwBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAYlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAarE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAojBlF;AA+DD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAc5E"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -48,6 +48,7 @@ import { gracefulShutdown } from "../server/shutdown.js";
|
|
|
48
48
|
import { getLiveMcpState } from "../mcp/state-getter.js";
|
|
49
49
|
import { preflightSqlite, SessionStore } from "../server/storage.js";
|
|
50
50
|
import { DevProcessRegistry } from "../server/dev-process-registry.js";
|
|
51
|
+
import { sweepOffloads } from "../server/offload-gc.js";
|
|
51
52
|
/** Default backend base URL when neither env nor override is set. */
|
|
52
53
|
const DEFAULT_BACKEND_URL = "https://api.aexol.ai";
|
|
53
54
|
import { getVersion } from "../version.js";
|
|
@@ -133,9 +134,21 @@ export async function runServe(opts = {}) {
|
|
|
133
134
|
process.stderr.write(`✗ ${preflight.error}\n`);
|
|
134
135
|
process.exit(1);
|
|
135
136
|
}
|
|
137
|
+
// Pre-flight: sweep stale large-result offload files (fire-and-forget).
|
|
136
138
|
const store = new SessionStore(dbPath);
|
|
137
139
|
const devProcessRegistry = new DevProcessRegistry(getAgentDir());
|
|
138
140
|
devProcessRegistry.reconcile();
|
|
141
|
+
// GC for stale large-result offload files. Deferred via setImmediate so it
|
|
142
|
+
// never blocks server startup; sweepOffloads() never throws, but the guard
|
|
143
|
+
// stays so a failed sweep can never take down serving.
|
|
144
|
+
setImmediate(() => {
|
|
145
|
+
try {
|
|
146
|
+
sweepOffloads();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Best-effort.
|
|
150
|
+
}
|
|
151
|
+
});
|
|
139
152
|
// Mutable holder for the meta-event publisher. Bound below once we
|
|
140
153
|
// have both `relay` (the send target) and `registration.record.machineId`
|
|
141
154
|
// (the canonical machineId the backend uses to fan out). The manager
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/console.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/console.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA2EhF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Console messages tool: browser_console_messages.
|
|
3
3
|
*/
|
|
4
|
+
import { offloadLargeResult } from "../../../sdk/coding-agent/core/tools/offload.js";
|
|
4
5
|
export function registerConsoleTools(ext, bs) {
|
|
5
6
|
const consoleTool = {
|
|
6
7
|
name: "browser_console_messages",
|
|
@@ -30,12 +31,31 @@ export function registerConsoleTools(ext, bs) {
|
|
|
30
31
|
}
|
|
31
32
|
const lines = messages.map((m) => `[${m.type.padEnd(7)}] ${m.text.slice(0, 300)}`);
|
|
32
33
|
const text = lines.join("\n");
|
|
34
|
+
// Small console dumps pass through untouched; large ones become
|
|
35
|
+
// preview + offload footer (fallback keeps the legacy cap).
|
|
36
|
+
let displayText = text;
|
|
37
|
+
let fullOutputPath;
|
|
38
|
+
let offloadTruncation;
|
|
39
|
+
try {
|
|
40
|
+
const offloaded = offloadLargeResult(text, "browser_console_messages", "shared");
|
|
41
|
+
displayText = offloaded.preview;
|
|
42
|
+
if (offloaded.fullOutputPath && offloaded.truncation) {
|
|
43
|
+
fullOutputPath = offloaded.fullOutputPath;
|
|
44
|
+
offloadTruncation = offloaded.truncation;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
displayText = text.length > 20_000 ? text.slice(0, 20_000) + "\n\n[truncated]" : text;
|
|
49
|
+
}
|
|
33
50
|
return {
|
|
34
51
|
content: [{
|
|
35
52
|
type: "text",
|
|
36
|
-
text:
|
|
53
|
+
text: displayText,
|
|
37
54
|
}],
|
|
38
|
-
details: {
|
|
55
|
+
details: {
|
|
56
|
+
...(fullOutputPath ? { fullOutputPath } : {}),
|
|
57
|
+
...(offloadTruncation ? { offloadTruncation } : {}),
|
|
58
|
+
},
|
|
39
59
|
};
|
|
40
60
|
}
|
|
41
61
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/evaluate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/evaluate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAuBvD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAkHjF"}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Evaluate tools: browser_evaluate, browser_run_code_unsafe.
|
|
3
3
|
*/
|
|
4
|
+
import { offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
|
|
5
|
+
function offloadBrowserText(text, toolName) {
|
|
6
|
+
try {
|
|
7
|
+
const offloaded = offloadLargeResult(text, toolName, "shared");
|
|
8
|
+
if (!offloaded.fullOutputPath || !offloaded.truncation)
|
|
9
|
+
return { text };
|
|
10
|
+
return {
|
|
11
|
+
text: offloaded.preview,
|
|
12
|
+
fullOutputPath: offloaded.fullOutputPath,
|
|
13
|
+
offloadTruncation: offloaded.truncation,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { text: text.length > 50000 ? text.slice(0, 50000) + "\n\n[truncated]" : text };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
4
20
|
export function registerEvaluateTools(ext, bs) {
|
|
5
21
|
const evaluateTool = {
|
|
6
22
|
name: "browser_evaluate",
|
|
@@ -34,12 +50,16 @@ export function registerEvaluateTools(ext, bs) {
|
|
|
34
50
|
const text = typeof result === "string"
|
|
35
51
|
? result
|
|
36
52
|
: JSON.stringify(result, null, 2);
|
|
53
|
+
const offloaded = offloadBrowserText(text, "browser_evaluate");
|
|
37
54
|
return {
|
|
38
55
|
content: [{
|
|
39
56
|
type: "text",
|
|
40
|
-
text:
|
|
57
|
+
text: offloaded.text,
|
|
41
58
|
}],
|
|
42
|
-
details: {
|
|
59
|
+
details: {
|
|
60
|
+
...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
|
|
61
|
+
...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
|
|
62
|
+
},
|
|
43
63
|
};
|
|
44
64
|
}
|
|
45
65
|
catch (err) {
|
|
@@ -79,12 +99,16 @@ export function registerEvaluateTools(ext, bs) {
|
|
|
79
99
|
const text = typeof result === "string"
|
|
80
100
|
? result
|
|
81
101
|
: JSON.stringify(result, null, 2);
|
|
102
|
+
const offloaded = offloadBrowserText(text, "browser_run_code_unsafe");
|
|
82
103
|
return {
|
|
83
104
|
content: [{
|
|
84
105
|
type: "text",
|
|
85
|
-
text:
|
|
106
|
+
text: offloaded.text,
|
|
86
107
|
}],
|
|
87
|
-
details: {
|
|
108
|
+
details: {
|
|
109
|
+
...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
|
|
110
|
+
...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
|
|
111
|
+
},
|
|
88
112
|
};
|
|
89
113
|
}
|
|
90
114
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/network.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/network.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAwBvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA4HhF"}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Network tools: browser_network_requests, browser_network_request.
|
|
3
3
|
*/
|
|
4
|
+
import { offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
|
|
5
|
+
function offloadBrowserText(text, toolName, legacyLimit) {
|
|
6
|
+
try {
|
|
7
|
+
const offloaded = offloadLargeResult(text, toolName, "shared");
|
|
8
|
+
if (!offloaded.fullOutputPath || !offloaded.truncation)
|
|
9
|
+
return { text };
|
|
10
|
+
return {
|
|
11
|
+
text: offloaded.preview,
|
|
12
|
+
fullOutputPath: offloaded.fullOutputPath,
|
|
13
|
+
offloadTruncation: offloaded.truncation,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { text: text.length > legacyLimit ? text.slice(0, legacyLimit) + "\n\n[truncated]" : text };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
4
20
|
export function registerNetworkTools(ext, bs) {
|
|
5
21
|
const networkRequestsTool = {
|
|
6
22
|
name: "browser_network_requests",
|
|
@@ -90,12 +106,16 @@ export function registerNetworkTools(ext, bs) {
|
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
108
|
const text = lines.join("\n");
|
|
109
|
+
const offloaded = offloadBrowserText(text, "browser_network_request", 50_000);
|
|
93
110
|
return {
|
|
94
111
|
content: [{
|
|
95
112
|
type: "text",
|
|
96
|
-
text:
|
|
113
|
+
text: offloaded.text,
|
|
97
114
|
}],
|
|
98
|
-
details: {
|
|
115
|
+
details: {
|
|
116
|
+
...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
|
|
117
|
+
...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
|
|
118
|
+
},
|
|
99
119
|
};
|
|
100
120
|
}
|
|
101
121
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAyC9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAqK7E"}
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { fetchPage } from "../utils/fetcher.js";
|
|
2
2
|
import { renderPage, isRendererAvailable } from "../utils/renderer.js";
|
|
3
|
-
import {
|
|
3
|
+
import { countLines, offloadLargeResult } from "../../../sdk/coding-agent/core/tools/offload.js";
|
|
4
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
5
|
+
/**
|
|
6
|
+
* Offload one large text field via the shared offload module (shared
|
|
7
|
+
* thresholds, head+tail preview + footer, owner-only file permissions).
|
|
8
|
+
* `shown` is always counted from the actually displayed preview text.
|
|
9
|
+
*/
|
|
10
|
+
function tryOffloadSeoContent(content, toolName, sessionId) {
|
|
11
|
+
if (!content)
|
|
12
|
+
return { text: "" };
|
|
13
|
+
let offloaded;
|
|
14
|
+
try {
|
|
15
|
+
offloaded = offloadLargeResult(content, toolName, sessionId);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { text: content };
|
|
19
|
+
}
|
|
20
|
+
if (!offloaded.fullOutputPath || !offloaded.truncation) {
|
|
21
|
+
return { text: content };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
text: offloaded.preview,
|
|
25
|
+
fullOutputPath: offloaded.fullOutputPath,
|
|
26
|
+
offloadTruncation: offloaded.truncation,
|
|
27
|
+
shownLines: countLines(offloaded.preview),
|
|
28
|
+
totalLines: countLines(content),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
4
31
|
export function registerFetchTools(ext, config) {
|
|
5
32
|
const fetchTool = {
|
|
6
33
|
name: "seo_fetch",
|
|
@@ -24,7 +51,7 @@ export function registerFetchTools(ext, config) {
|
|
|
24
51
|
},
|
|
25
52
|
required: ["url"],
|
|
26
53
|
},
|
|
27
|
-
async execute(_toolCallId, params) {
|
|
54
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
28
55
|
const { url, userAgent, timeout, followRedirects } = params;
|
|
29
56
|
try {
|
|
30
57
|
const result = await fetchPage(url, config, {
|
|
@@ -35,18 +62,26 @@ export function registerFetchTools(ext, config) {
|
|
|
35
62
|
if (result.error) {
|
|
36
63
|
return errorResult(`Fetch failed: ${result.error}`, { result });
|
|
37
64
|
}
|
|
65
|
+
const sessionId = ctx
|
|
66
|
+
?.sessionManager?.getSessionId?.() ?? "shared";
|
|
67
|
+
const offloaded = tryOffloadSeoContent(result.content, "seo_fetch", sessionId);
|
|
38
68
|
const summary = `✓ Fetched: ${result.finalUrl}\n` +
|
|
39
69
|
`Status: ${result.statusCode} ${result.statusText}\n` +
|
|
40
70
|
`Redirects: ${result.redirectChain.length}\n` +
|
|
41
71
|
`Content-Length: ${result.contentLengthBytes} bytes\n` +
|
|
42
|
-
`Headers: ${Object.keys(result.headers).join(", ")}
|
|
72
|
+
`Headers: ${Object.keys(result.headers).join(", ")}` +
|
|
73
|
+
(offloaded.fullOutputPath ? `\nFull output: ${offloaded.fullOutputPath}` : "");
|
|
43
74
|
return textResult(summary, {
|
|
44
75
|
url: result.url,
|
|
45
76
|
finalUrl: result.finalUrl,
|
|
46
77
|
statusCode: result.statusCode,
|
|
47
|
-
content:
|
|
78
|
+
content: offloaded.text,
|
|
48
79
|
headers: result.headers,
|
|
49
80
|
redirectChain: result.redirectChain,
|
|
81
|
+
...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
|
|
82
|
+
...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
|
|
83
|
+
...(offloaded.shownLines !== undefined ? { shownLines: offloaded.shownLines } : {}),
|
|
84
|
+
...(offloaded.totalLines !== undefined ? { totalLines: offloaded.totalLines } : {}),
|
|
50
85
|
});
|
|
51
86
|
}
|
|
52
87
|
catch (err) {
|
|
@@ -83,7 +118,7 @@ export function registerFetchTools(ext, config) {
|
|
|
83
118
|
},
|
|
84
119
|
required: ["url"],
|
|
85
120
|
},
|
|
86
|
-
async execute(_toolCallId, params) {
|
|
121
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
87
122
|
const { url, viewport, captureA11y, captureConsole, timeout } = params;
|
|
88
123
|
const available = await isRendererAvailable();
|
|
89
124
|
if (!available) {
|
|
@@ -96,21 +131,37 @@ export function registerFetchTools(ext, config) {
|
|
|
96
131
|
captureConsole,
|
|
97
132
|
timeout,
|
|
98
133
|
});
|
|
134
|
+
const sessionId = ctx
|
|
135
|
+
?.sessionManager?.getSessionId?.() ?? "shared";
|
|
136
|
+
const contentOffload = tryOffloadSeoContent(result.content, "seo_render", sessionId);
|
|
137
|
+
const rawOffload = tryOffloadSeoContent(result.rawContent, "seo_render", sessionId);
|
|
138
|
+
const offloadNote = contentOffload.fullOutputPath || rawOffload.fullOutputPath
|
|
139
|
+
? `\nFull output: ${[contentOffload.fullOutputPath, rawOffload.fullOutputPath].filter(Boolean).join(", ")}`
|
|
140
|
+
: "";
|
|
99
141
|
const summary = `✓ Rendered: ${result.finalUrl}\n` +
|
|
100
142
|
`Status: ${result.statusCode}\n` +
|
|
101
143
|
`SPA: ${result.isSpa}\n` +
|
|
102
144
|
`Render time: ${result.renderMs}ms\n` +
|
|
103
|
-
`Console errors: ${result.consoleErrors.length}
|
|
145
|
+
`Console errors: ${result.consoleErrors.length}` + offloadNote;
|
|
146
|
+
// Keep BOTH offload paths — content and rawContent are independent
|
|
147
|
+
// fields and each may need re-reading.
|
|
148
|
+
const fullOutputPaths = [contentOffload.fullOutputPath, rawOffload.fullOutputPath].filter((p) => Boolean(p));
|
|
104
149
|
return textResult(summary, {
|
|
105
150
|
url: result.url,
|
|
106
151
|
finalUrl: result.finalUrl,
|
|
107
152
|
statusCode: result.statusCode,
|
|
108
|
-
content:
|
|
109
|
-
rawContent:
|
|
153
|
+
content: contentOffload.text,
|
|
154
|
+
rawContent: rawOffload.text,
|
|
110
155
|
isSpa: result.isSpa,
|
|
111
156
|
consoleErrors: result.consoleErrors,
|
|
112
157
|
renderMs: result.renderMs,
|
|
113
158
|
accessibilityTree: result.accessibilityTree,
|
|
159
|
+
...(fullOutputPaths.length > 0
|
|
160
|
+
? { fullOutputPath: fullOutputPaths[0], fullOutputPaths }
|
|
161
|
+
: {}),
|
|
162
|
+
...(contentOffload.offloadTruncation
|
|
163
|
+
? { offloadTruncation: contentOffload.offloadTruncation }
|
|
164
|
+
: {}),
|
|
114
165
|
});
|
|
115
166
|
}
|
|
116
167
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AAgCxG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAwE5D"}
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* web_fetch tool definition.
|
|
3
3
|
*/
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import {
|
|
5
|
+
import { isAlreadyOffloaded, offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
|
|
6
6
|
import { fetchWebPage, WebFetchError } from "../utils/fetcher.js";
|
|
7
7
|
import { convertContent } from "../utils/parser.js";
|
|
8
|
-
const MAX_OUTPUT_LINES = 4000;
|
|
9
|
-
const MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
|
|
10
8
|
const webFetchSchema = Type.Object({
|
|
11
9
|
url: Type.String({
|
|
12
10
|
description: "The URL to fetch. Must begin with http:// or https://.",
|
|
@@ -32,7 +30,7 @@ export function registerWebFetchTool(ext) {
|
|
|
32
30
|
"Supports HTTP/HTTPS only and rejects binary content.",
|
|
33
31
|
promptSnippet: "`web_fetch { url, format?, timeout? }` — fetch a web page as markdown/text/HTML/JSON",
|
|
34
32
|
parameters: webFetchSchema,
|
|
35
|
-
async execute(_toolCallId, params, signal, _onUpdate,
|
|
33
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
36
34
|
const { url, format = "markdown", timeout = 30 } = params;
|
|
37
35
|
try {
|
|
38
36
|
const fetchResult = await fetchWebPage(url, {
|
|
@@ -40,19 +38,33 @@ export function registerWebFetchTool(ext) {
|
|
|
40
38
|
signal,
|
|
41
39
|
});
|
|
42
40
|
const converted = await convertContent(fetchResult.content, format);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Single truncation system: offloadLargeResult decides by its own
|
|
42
|
+
// byte/line thresholds and builds the head+tail preview + footer.
|
|
43
|
+
// The full content is passed directly — no pre-truncation here.
|
|
44
|
+
let fullOutputPath;
|
|
45
|
+
let output = converted;
|
|
46
|
+
let offloadTruncation;
|
|
47
|
+
if (!isAlreadyOffloaded(converted)) {
|
|
48
|
+
try {
|
|
49
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? "shared";
|
|
50
|
+
const offloaded = offloadLargeResult(converted, "web_fetch", sessionId);
|
|
51
|
+
output = offloaded.preview;
|
|
52
|
+
if (offloaded.fullOutputPath) {
|
|
53
|
+
fullOutputPath = offloaded.fullOutputPath;
|
|
54
|
+
offloadTruncation = offloaded.truncation ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort — serve the full content when offload fails.
|
|
59
|
+
output = converted;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const truncated = fullOutputPath !== undefined;
|
|
51
63
|
return {
|
|
52
64
|
content: [
|
|
53
65
|
{
|
|
54
66
|
type: "text",
|
|
55
|
-
text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes
|
|
67
|
+
text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes\n\n${output}`,
|
|
56
68
|
},
|
|
57
69
|
],
|
|
58
70
|
details: {
|
|
@@ -61,8 +73,9 @@ export function registerWebFetchTool(ext) {
|
|
|
61
73
|
contentType: fetchResult.contentType,
|
|
62
74
|
format,
|
|
63
75
|
output,
|
|
64
|
-
truncated
|
|
65
|
-
|
|
76
|
+
truncated,
|
|
77
|
+
...(fullOutputPath ? { fullOutputPath } : {}),
|
|
78
|
+
...(offloadTruncation ? { offloadTruncation } : {}),
|
|
66
79
|
},
|
|
67
80
|
};
|
|
68
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-output-compressor.d.ts","sourceRoot":"","sources":["../../src/memory/tool-output-compressor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAEX,gBAAgB,EAIhB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-output-compressor.d.ts","sourceRoot":"","sources":["../../src/memory/tool-output-compressor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAEX,gBAAgB,EAIhB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAQ5C,MAAM,MAAM,gBAAgB,GAAG,0BAA0B,CAAC;AAE1D,eAAO,MAAM,yBAAyB,EAAE,gBAEvC,CAAC;AAgsBF,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AA8BD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,OAAO,GAAG,8BAA8B,CAElG;AAED,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1E;AAgGD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,EAC7F,GAAG,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAC7C,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAYvG;AAED,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,OAAO,GACd,IAAI,CAmCN"}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { DEFAULT_COMPACTION_POLICY, } from "../sdk/coding-agent/core/compaction/policy.js";
|
|
31
31
|
import { debugLog } from "./debug-log.js";
|
|
32
|
+
import { isAlreadyOffloaded, offloadLargeResult } from "../sdk/coding-agent/core/tools/offload.js";
|
|
32
33
|
export const DEFAULT_COMPRESSOR_CONFIG = {
|
|
33
34
|
...DEFAULT_COMPACTION_POLICY.toolOutputCompressor,
|
|
34
35
|
};
|
|
@@ -672,6 +673,55 @@ function resolveCompressorConfig(ctx) {
|
|
|
672
673
|
const manager = ctx.settingsManager;
|
|
673
674
|
return manager?.getCompactionPolicy?.().toolOutputCompressor ?? DEFAULT_COMPRESSOR_CONFIG;
|
|
674
675
|
}
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// Large-result offload fallback
|
|
678
|
+
// ============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* Fallback offload for tool types without native preview+file handling.
|
|
681
|
+
*
|
|
682
|
+
* - Skips read/bash (they already compress natively) and anything already
|
|
683
|
+
* carrying a `Full output:` footer (notably the bash offload path).
|
|
684
|
+
* - Skips error results and non-text/binary content.
|
|
685
|
+
* - Persists the full text via `offloadLargeResult()` and swaps the event
|
|
686
|
+
* text for preview + footer. Small results pass through untouched.
|
|
687
|
+
*
|
|
688
|
+
* The `tool_result` hook carries no session id, so offloads land under the
|
|
689
|
+
* shared `offloads/shared/` namespace.
|
|
690
|
+
*/
|
|
691
|
+
function maybeOffloadLargeResult(event) {
|
|
692
|
+
if (event.toolName === "read" || event.toolName === "bash")
|
|
693
|
+
return undefined;
|
|
694
|
+
if (event.isError)
|
|
695
|
+
return undefined;
|
|
696
|
+
const texts = event.content.filter((c) => c.type === "text" && typeof c.text === "string");
|
|
697
|
+
if (texts.length === 0)
|
|
698
|
+
return undefined;
|
|
699
|
+
const original = texts.map((c) => c.text).join("\n");
|
|
700
|
+
if (!original || original.includes("\0"))
|
|
701
|
+
return undefined;
|
|
702
|
+
if (isAlreadyOffloaded(original))
|
|
703
|
+
return undefined;
|
|
704
|
+
let offloaded;
|
|
705
|
+
try {
|
|
706
|
+
offloaded = offloadLargeResult(original, event.toolName, "shared");
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
if (!offloaded.fullOutputPath || !offloaded.truncation)
|
|
712
|
+
return undefined;
|
|
713
|
+
return {
|
|
714
|
+
content: [
|
|
715
|
+
...event.content.filter((c) => c.type !== "text"),
|
|
716
|
+
{ type: "text", text: offloaded.preview },
|
|
717
|
+
],
|
|
718
|
+
details: {
|
|
719
|
+
...(typeof event.details === "object" && event.details !== null ? event.details : {}),
|
|
720
|
+
fullOutputPath: offloaded.fullOutputPath,
|
|
721
|
+
offloadTruncation: offloaded.truncation,
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
675
725
|
function compressedContentResult(runtime, event, toolName, original, compressed) {
|
|
676
726
|
if (!compressed || compressed === original)
|
|
677
727
|
return undefined;
|
|
@@ -734,7 +784,8 @@ export function registerToolOutputCompressor(ext, runtime) {
|
|
|
734
784
|
const compressed = compressBashOutput(event, config);
|
|
735
785
|
return compressedContentResult(runtime, event, "bash", original, compressed);
|
|
736
786
|
}
|
|
737
|
-
//
|
|
738
|
-
|
|
787
|
+
// All other tool types (web_fetch, browser_*, seo_*, pdf_*, …): fall back
|
|
788
|
+
// to large-result offloading so no truncated output is ever lost.
|
|
789
|
+
return maybeOffloadLargeResult(_event);
|
|
739
790
|
});
|
|
740
791
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history-projection.d.ts","sourceRoot":"","sources":["../../src/relay/history-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAEpD,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;
|
|
1
|
+
{"version":3,"file":"history-projection.d.ts","sourceRoot":"","sources":["../../src/relay/history-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAEpD,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;AAwMhF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwC9D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAMtF"}
|
|
@@ -72,6 +72,13 @@ function projectToolResult(event) {
|
|
|
72
72
|
compactDetails.status = details.status;
|
|
73
73
|
if ("mode" in details)
|
|
74
74
|
compactDetails.mode = details.mode;
|
|
75
|
+
// Preserve offload pointers so the agent can re-read full outputs.
|
|
76
|
+
if ("fullOutputPath" in details)
|
|
77
|
+
compactDetails.fullOutputPath = details.fullOutputPath;
|
|
78
|
+
if ("offloadTruncation" in details)
|
|
79
|
+
compactDetails.offloadTruncation = details.offloadTruncation;
|
|
80
|
+
if ("truncation" in details)
|
|
81
|
+
compactDetails.truncation = truncateStrings(details.truncation);
|
|
75
82
|
projectedResult = { ...result, details: compactDetails };
|
|
76
83
|
}
|
|
77
84
|
if (typeof projectedResult.content === "string") {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large-result offloading for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Goal: no truncated tool result is ever lost. The full output goes to a
|
|
5
|
+
* file under `<configDir>/offloads/<sessionId>/`, and only a head+tail
|
|
6
|
+
* preview plus a footer (path + re-read instructions) enters the LLM context.
|
|
7
|
+
* The agent can then page through the file with `read` (offset/limit) or
|
|
8
|
+
* `bash` (sed/head/tail).
|
|
9
|
+
*
|
|
10
|
+
* This is the generic version of the pattern pioneered by the bash tool
|
|
11
|
+
* (preview + `Full output: <path>` footer). Small results pass through
|
|
12
|
+
* untouched — no file, no overhead. Binary-looking content (NUL bytes) is
|
|
13
|
+
* never offloaded.
|
|
14
|
+
*/
|
|
15
|
+
/** Default byte threshold above which a result is offloaded to disk. */
|
|
16
|
+
export declare const DEFAULT_OFFLOAD_BYTES: number;
|
|
17
|
+
/** Default line threshold above which a result is offloaded to disk. */
|
|
18
|
+
export declare const DEFAULT_OFFLOAD_LINES = 2000;
|
|
19
|
+
/** Preview head size (lines kept from the start). */
|
|
20
|
+
export declare const PREVIEW_HEAD_LINES = 100;
|
|
21
|
+
/** Preview tail size (lines kept from the end). */
|
|
22
|
+
export declare const PREVIEW_TAIL_LINES = 100;
|
|
23
|
+
/** Emergency byte cap for previews (head+tail can still be huge with long lines). */
|
|
24
|
+
export declare const PREVIEW_MAX_BYTES: number;
|
|
25
|
+
/** Marker identifying an already-offloaded result (bash tool compat). */
|
|
26
|
+
export declare const OFFLOAD_MARKER = "Full output:";
|
|
27
|
+
/** Anchored marker check: only a real `Full output: <path>.log` footer counts. */
|
|
28
|
+
export declare const OFFLOAD_PATH_RE: RegExp;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a positive-int env override. Returns `fallback` when the variable is
|
|
31
|
+
* unset, unparseable, zero, or negative — a non-positive budget would
|
|
32
|
+
* otherwise disable offloading/GC entirely, so it is treated as "not set".
|
|
33
|
+
*/
|
|
34
|
+
export declare function envPositiveInt(name: string, fallback: number): number;
|
|
35
|
+
/** Effective byte limit (override via `SPECTRAL_OFFLOAD_BYTES`). */
|
|
36
|
+
export declare function getOffloadByteLimit(): number;
|
|
37
|
+
/** Effective line limit (override via `SPECTRAL_OFFLOAD_LINES`). */
|
|
38
|
+
export declare function getOffloadLineLimit(): number;
|
|
39
|
+
export interface OffloadTruncation {
|
|
40
|
+
originalBytes: number;
|
|
41
|
+
shownBytes: number;
|
|
42
|
+
headLines: number;
|
|
43
|
+
tailLines: number;
|
|
44
|
+
omittedLines: number;
|
|
45
|
+
totalLines: number;
|
|
46
|
+
fullOutputPath: string;
|
|
47
|
+
}
|
|
48
|
+
export interface OffloadResult {
|
|
49
|
+
/** Model-facing text: full original for small results, preview+footer for large ones. */
|
|
50
|
+
preview: string;
|
|
51
|
+
/** Absolute path of the persisted full output, or null when nothing was written. */
|
|
52
|
+
fullOutputPath: string | null;
|
|
53
|
+
/** Truncation metadata, or null when nothing was offloaded. */
|
|
54
|
+
truncation: OffloadTruncation | null;
|
|
55
|
+
}
|
|
56
|
+
export declare function countLines(text: string): number;
|
|
57
|
+
/** Marker for results that were already offloaded (or produced by the bash tool). */
|
|
58
|
+
export declare function isAlreadyOffloaded(text: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether `text` is large enough to deserve offloading.
|
|
61
|
+
* Binary-looking content (NUL bytes) is never offloaded.
|
|
62
|
+
*/
|
|
63
|
+
export declare function shouldOffload(text: string): boolean;
|
|
64
|
+
export interface PreviewResult {
|
|
65
|
+
preview: string;
|
|
66
|
+
headLines: number;
|
|
67
|
+
tailLines: number;
|
|
68
|
+
omittedLines: number;
|
|
69
|
+
totalLines: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a head+tail preview. Never cuts in the middle of a line (except via
|
|
73
|
+
* the emergency byte cap below); the gap is replaced with a
|
|
74
|
+
* `[... N lines omitted ...]` marker. When head+tail itself exceeds
|
|
75
|
+
* `PREVIEW_MAX_BYTES` (e.g. a single giant line), the preview is cut
|
|
76
|
+
* mid-line with a `[... N bytes omitted ...]` marker so the LLM context
|
|
77
|
+
* never receives an unbounded preview.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildPreview(text: string, headLines?: number, tailLines?: number): PreviewResult;
|
|
80
|
+
export interface OffloadFooterOptions {
|
|
81
|
+
headLines?: number;
|
|
82
|
+
tailLines?: number;
|
|
83
|
+
totalLines?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Single-line footer appended to a preview. Carries the full-output path plus
|
|
87
|
+
* a copy-pasteable re-read hint for the agent.
|
|
88
|
+
*/
|
|
89
|
+
export declare function formatOffloadFooter(path: string, shown: number, total: number, opts?: OffloadFooterOptions): string;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve (and create) the offload file path for a tool result:
|
|
92
|
+
* `<configDir>/offloads/<sessionId>/<tool>-<timestamp>-<rand>.log`.
|
|
93
|
+
* Honors `SPECTRAL_CONFIG_DIR` via `getConfigDir()`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveOffloadPath(sessionId: string, toolName: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* Offload `text` to disk when it exceeds the size thresholds.
|
|
98
|
+
* Small (or binary, or already-offloaded) results are returned unchanged
|
|
99
|
+
* with `fullOutputPath: null` — no file, no overhead.
|
|
100
|
+
*/
|
|
101
|
+
export declare function offloadLargeResult(text: string, toolName: string, sessionId: string): OffloadResult;
|
|
102
|
+
//# sourceMappingURL=offload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offload.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/offload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,QAAY,CAAC;AAC/C,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,QAAoB,CAAC;AACnD,yEAAyE;AACzE,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAC7C,kFAAkF;AAClF,eAAO,MAAM,eAAe,QAA0B,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,MAAM,WAAW,iBAAiB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKnD;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,EACtC,SAAS,GAAE,MAA2B,GACpC,aAAa,CAmBf;AAmCD,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAyB,GAC7B,MAAM,CAiBR;AAOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CA6BnG"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large-result offloading for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Goal: no truncated tool result is ever lost. The full output goes to a
|
|
5
|
+
* file under `<configDir>/offloads/<sessionId>/`, and only a head+tail
|
|
6
|
+
* preview plus a footer (path + re-read instructions) enters the LLM context.
|
|
7
|
+
* The agent can then page through the file with `read` (offset/limit) or
|
|
8
|
+
* `bash` (sed/head/tail).
|
|
9
|
+
*
|
|
10
|
+
* This is the generic version of the pattern pioneered by the bash tool
|
|
11
|
+
* (preview + `Full output: <path>` footer). Small results pass through
|
|
12
|
+
* untouched — no file, no overhead. Binary-looking content (NUL bytes) is
|
|
13
|
+
* never offloaded.
|
|
14
|
+
*/
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { getConfigDir } from "../../../../config.js";
|
|
19
|
+
import { DEFAULT_MAX_BYTES } from "./truncate.js";
|
|
20
|
+
/** Default byte threshold above which a result is offloaded to disk. */
|
|
21
|
+
export const DEFAULT_OFFLOAD_BYTES = 50 * 1024; // 50KB
|
|
22
|
+
/** Default line threshold above which a result is offloaded to disk. */
|
|
23
|
+
export const DEFAULT_OFFLOAD_LINES = 2000;
|
|
24
|
+
/** Preview head size (lines kept from the start). */
|
|
25
|
+
export const PREVIEW_HEAD_LINES = 100;
|
|
26
|
+
/** Preview tail size (lines kept from the end). */
|
|
27
|
+
export const PREVIEW_TAIL_LINES = 100;
|
|
28
|
+
/** Emergency byte cap for previews (head+tail can still be huge with long lines). */
|
|
29
|
+
export const PREVIEW_MAX_BYTES = DEFAULT_MAX_BYTES;
|
|
30
|
+
/** Marker identifying an already-offloaded result (bash tool compat). */
|
|
31
|
+
export const OFFLOAD_MARKER = "Full output:";
|
|
32
|
+
/** Anchored marker check: only a real `Full output: <path>.log` footer counts. */
|
|
33
|
+
export const OFFLOAD_PATH_RE = /Full output: \S+\.log/;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a positive-int env override. Returns `fallback` when the variable is
|
|
36
|
+
* unset, unparseable, zero, or negative — a non-positive budget would
|
|
37
|
+
* otherwise disable offloading/GC entirely, so it is treated as "not set".
|
|
38
|
+
*/
|
|
39
|
+
export function envPositiveInt(name, fallback) {
|
|
40
|
+
const raw = process.env[name];
|
|
41
|
+
if (!raw)
|
|
42
|
+
return fallback;
|
|
43
|
+
const parsed = Number.parseInt(raw, 10);
|
|
44
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
45
|
+
}
|
|
46
|
+
/** Effective byte limit (override via `SPECTRAL_OFFLOAD_BYTES`). */
|
|
47
|
+
export function getOffloadByteLimit() {
|
|
48
|
+
return envPositiveInt("SPECTRAL_OFFLOAD_BYTES", DEFAULT_OFFLOAD_BYTES);
|
|
49
|
+
}
|
|
50
|
+
/** Effective line limit (override via `SPECTRAL_OFFLOAD_LINES`). */
|
|
51
|
+
export function getOffloadLineLimit() {
|
|
52
|
+
return envPositiveInt("SPECTRAL_OFFLOAD_LINES", DEFAULT_OFFLOAD_LINES);
|
|
53
|
+
}
|
|
54
|
+
export function countLines(text) {
|
|
55
|
+
if (text.length === 0)
|
|
56
|
+
return 0;
|
|
57
|
+
const lines = text.split("\n");
|
|
58
|
+
// A trailing newline does not start a new line.
|
|
59
|
+
return text.endsWith("\n") ? lines.length - 1 : lines.length;
|
|
60
|
+
}
|
|
61
|
+
/** Marker for results that were already offloaded (or produced by the bash tool). */
|
|
62
|
+
export function isAlreadyOffloaded(text) {
|
|
63
|
+
return OFFLOAD_PATH_RE.test(text);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether `text` is large enough to deserve offloading.
|
|
67
|
+
* Binary-looking content (NUL bytes) is never offloaded.
|
|
68
|
+
*/
|
|
69
|
+
export function shouldOffload(text) {
|
|
70
|
+
if (!text || text.includes("\0"))
|
|
71
|
+
return false;
|
|
72
|
+
if (isAlreadyOffloaded(text))
|
|
73
|
+
return false;
|
|
74
|
+
if (Buffer.byteLength(text, "utf-8") > getOffloadByteLimit())
|
|
75
|
+
return true;
|
|
76
|
+
return countLines(text) > getOffloadLineLimit();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build a head+tail preview. Never cuts in the middle of a line (except via
|
|
80
|
+
* the emergency byte cap below); the gap is replaced with a
|
|
81
|
+
* `[... N lines omitted ...]` marker. When head+tail itself exceeds
|
|
82
|
+
* `PREVIEW_MAX_BYTES` (e.g. a single giant line), the preview is cut
|
|
83
|
+
* mid-line with a `[... N bytes omitted ...]` marker so the LLM context
|
|
84
|
+
* never receives an unbounded preview.
|
|
85
|
+
*/
|
|
86
|
+
export function buildPreview(text, headLines = PREVIEW_HEAD_LINES, tailLines = PREVIEW_TAIL_LINES) {
|
|
87
|
+
const rawLines = text.split("\n");
|
|
88
|
+
// Drop the phantom empty element from a trailing newline.
|
|
89
|
+
const lines = text.endsWith("\n") && rawLines.length > 0 ? rawLines.slice(0, -1) : rawLines;
|
|
90
|
+
const totalLines = lines.length;
|
|
91
|
+
if (totalLines <= headLines + tailLines) {
|
|
92
|
+
return capPreviewBytes(text, {
|
|
93
|
+
preview: text,
|
|
94
|
+
headLines: totalLines,
|
|
95
|
+
tailLines: 0,
|
|
96
|
+
omittedLines: 0,
|
|
97
|
+
totalLines,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const head = lines.slice(0, headLines);
|
|
101
|
+
const tail = lines.slice(totalLines - tailLines);
|
|
102
|
+
const omittedLines = totalLines - headLines - tailLines;
|
|
103
|
+
const preview = [...head, `[... ${omittedLines} lines omitted ...]`, ...tail].join("\n");
|
|
104
|
+
return capPreviewBytes(preview, { preview, headLines, tailLines, omittedLines, totalLines });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Emergency byte cap for previews. Long-line blobs (e.g. minified JSON on a
|
|
108
|
+
* single line) can make head+tail previews arbitrarily large; cut them
|
|
109
|
+
* mid-line with a byte-omitted marker. UTF-8 safe via Buffer slicing.
|
|
110
|
+
*/
|
|
111
|
+
function capPreviewBytes(textOrResult, fallback) {
|
|
112
|
+
const text = typeof textOrResult === "string" ? textOrResult : textOrResult.preview;
|
|
113
|
+
const base = typeof textOrResult === "string"
|
|
114
|
+
? fallback
|
|
115
|
+
: {
|
|
116
|
+
preview: textOrResult.preview,
|
|
117
|
+
headLines: textOrResult.headLines,
|
|
118
|
+
tailLines: textOrResult.tailLines,
|
|
119
|
+
omittedLines: textOrResult.omittedLines,
|
|
120
|
+
totalLines: textOrResult.totalLines,
|
|
121
|
+
};
|
|
122
|
+
if (Buffer.byteLength(text, "utf-8") <= PREVIEW_MAX_BYTES) {
|
|
123
|
+
return { ...base, preview: text };
|
|
124
|
+
}
|
|
125
|
+
const buf = Buffer.from(text, "utf-8");
|
|
126
|
+
// Leave room for the marker; walk back to a UTF-8 char boundary.
|
|
127
|
+
const markerReserve = 64;
|
|
128
|
+
let end = Math.max(0, PREVIEW_MAX_BYTES - markerReserve);
|
|
129
|
+
while (end > 0 && end < buf.length && (buf[end] & 0xc0) === 0x80)
|
|
130
|
+
end--;
|
|
131
|
+
const omitted = buf.length - end;
|
|
132
|
+
const preview = buf.subarray(0, end).toString("utf-8") + `\n[... ${omitted} bytes omitted ...]`;
|
|
133
|
+
return { ...base, preview };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Single-line footer appended to a preview. Carries the full-output path plus
|
|
137
|
+
* a copy-pasteable re-read hint for the agent.
|
|
138
|
+
*/
|
|
139
|
+
export function formatOffloadFooter(path, shown, total, opts = {}) {
|
|
140
|
+
const headLines = opts.headLines ?? PREVIEW_HEAD_LINES;
|
|
141
|
+
const tailLines = opts.tailLines ?? PREVIEW_TAIL_LINES;
|
|
142
|
+
// Guard against inconsistent callers (e.g. byte-counted `shown` on a
|
|
143
|
+
// line-based total): never claim to show more than exists.
|
|
144
|
+
const safeShown = Math.min(shown, total);
|
|
145
|
+
const safeTotal = Math.max(total, 0);
|
|
146
|
+
let readHint;
|
|
147
|
+
if (opts.totalLines !== undefined && opts.totalLines > headLines + tailLines) {
|
|
148
|
+
const tailStart = opts.totalLines - tailLines + 1;
|
|
149
|
+
readHint =
|
|
150
|
+
`Read with: sed -n '1,${headLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES} ` +
|
|
151
|
+
`or sed -n '${tailStart},${opts.totalLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES}`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
readHint = `Read with: sed -n '1,${headLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES}`;
|
|
155
|
+
}
|
|
156
|
+
return `[Truncated: showing ${safeShown} of ${safeTotal} bytes. Full output: ${path}. ${readHint}]`;
|
|
157
|
+
}
|
|
158
|
+
function sanitizeSegment(segment) {
|
|
159
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
160
|
+
return cleaned || "shared";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve (and create) the offload file path for a tool result:
|
|
164
|
+
* `<configDir>/offloads/<sessionId>/<tool>-<timestamp>-<rand>.log`.
|
|
165
|
+
* Honors `SPECTRAL_CONFIG_DIR` via `getConfigDir()`.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveOffloadPath(sessionId, toolName) {
|
|
168
|
+
const dir = join(getConfigDir(), "offloads", sanitizeSegment(sessionId));
|
|
169
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
170
|
+
const stamp = `${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
171
|
+
return join(dir, `${sanitizeSegment(toolName)}-${stamp}.log`);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Offload `text` to disk when it exceeds the size thresholds.
|
|
175
|
+
* Small (or binary, or already-offloaded) results are returned unchanged
|
|
176
|
+
* with `fullOutputPath: null` — no file, no overhead.
|
|
177
|
+
*/
|
|
178
|
+
export function offloadLargeResult(text, toolName, sessionId) {
|
|
179
|
+
if (!shouldOffload(text)) {
|
|
180
|
+
return { preview: text, fullOutputPath: null, truncation: null };
|
|
181
|
+
}
|
|
182
|
+
const originalBytes = Buffer.byteLength(text, "utf-8");
|
|
183
|
+
const { preview, headLines, tailLines, omittedLines, totalLines } = buildPreview(text);
|
|
184
|
+
const fullOutputPath = resolveOffloadPath(sessionId, toolName);
|
|
185
|
+
// Owner-only: offloads may contain secrets (headers, tokens, page bodies).
|
|
186
|
+
writeFileSync(fullOutputPath, text, { encoding: "utf-8", mode: 0o600 });
|
|
187
|
+
// `shown` is measured on the actually displayed preview text.
|
|
188
|
+
const footer = formatOffloadFooter(fullOutputPath, Buffer.byteLength(preview, "utf-8"), originalBytes, {
|
|
189
|
+
headLines,
|
|
190
|
+
tailLines,
|
|
191
|
+
totalLines,
|
|
192
|
+
});
|
|
193
|
+
const previewWithFooter = `${preview}\n${footer}`;
|
|
194
|
+
return {
|
|
195
|
+
preview: previewWithFooter,
|
|
196
|
+
fullOutputPath,
|
|
197
|
+
truncation: {
|
|
198
|
+
originalBytes,
|
|
199
|
+
shownBytes: Buffer.byteLength(previewWithFooter, "utf-8"),
|
|
200
|
+
headLines,
|
|
201
|
+
tailLines,
|
|
202
|
+
omittedLines,
|
|
203
|
+
totalLines,
|
|
204
|
+
fullOutputPath,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAaH,OAAO,EAQL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAatC,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAatC,OAAO,EACL,kBAAkB,IAAI,yBAAyB,EAEhD,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,UAAU,4BAA4B;IACpC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CACpC;AAiBD,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,yBAAyB,CAAC;AAmCpE,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACnC;;;;;OAKG;IACH,0BAA0B,EAAE,CAAC,GAAG,EAAE;QAChC,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,6EAA6E;QAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC9C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,yBAAyB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;
|
|
1
|
+
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -446,6 +446,13 @@ function parseWireToolEvents(eventsJsonl) {
|
|
|
446
446
|
* images and other rich results.
|
|
447
447
|
*/
|
|
448
448
|
const MAX_REHYDRATED_TEXT = 100_000;
|
|
449
|
+
/**
|
|
450
|
+
* Ceiling for blocks that already carry an offload footer (`Full output:`).
|
|
451
|
+
* These blocks are a bounded preview + a re-readable path, so cutting them
|
|
452
|
+
* with the generic limit would destroy the footer. Keep them whole up to
|
|
453
|
+
* this size; larger ones lose their middle but keep head + footer.
|
|
454
|
+
*/
|
|
455
|
+
const MAX_REHYDRATED_OFFLOAD_TEXT = 200_000;
|
|
449
456
|
function sanitizeRehydratedBlock(block) {
|
|
450
457
|
// Tool-result image blocks are not accepted by every provider; the raw
|
|
451
458
|
// result remains in events_jsonl for the UI, while the model gets a compact reference.
|
|
@@ -460,6 +467,20 @@ function sanitizeRehydratedBlock(block) {
|
|
|
460
467
|
// Truncate oversized text blocks to prevent context overflow.
|
|
461
468
|
if (block.type !== "text")
|
|
462
469
|
return block;
|
|
470
|
+
if (block.text.includes("Full output:")) {
|
|
471
|
+
if (block.text.length <= MAX_REHYDRATED_OFFLOAD_TEXT)
|
|
472
|
+
return block;
|
|
473
|
+
// Keep the head (preview) and the tail (offload footer), drop the middle.
|
|
474
|
+
const keepHead = Math.floor(MAX_REHYDRATED_OFFLOAD_TEXT * 0.7);
|
|
475
|
+
const keepTail = MAX_REHYDRATED_OFFLOAD_TEXT - keepHead;
|
|
476
|
+
const omitted = block.text.length - keepHead - keepTail;
|
|
477
|
+
return {
|
|
478
|
+
...block,
|
|
479
|
+
text: block.text.slice(0, keepHead) +
|
|
480
|
+
`\n\n[... ${omitted.toLocaleString()} bytes omitted from rehydrated offload preview ...]\n\n` +
|
|
481
|
+
block.text.slice(block.text.length - keepTail),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
463
484
|
if (block.text.length <= MAX_REHYDRATED_TEXT)
|
|
464
485
|
return block;
|
|
465
486
|
const omitted = block.text.length - MAX_REHYDRATED_TEXT;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage collection for large-result offload files.
|
|
3
|
+
*
|
|
4
|
+
* Offloads live under `<configDir>/offloads/<sessionId>/*.log`. This sweep
|
|
5
|
+
* removes per-session directories older than `SPECTRAL_OFFLOAD_TTL_DAYS`
|
|
6
|
+
* (default 7) and, when the total still exceeds `SPECTRAL_OFFLOAD_MAX_MB`
|
|
7
|
+
* (default 500), evicts the oldest sessions first.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort and synchronous: callers fire-and-forget it during preflight.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_OFFLOAD_TTL_DAYS = 7;
|
|
12
|
+
export declare const DEFAULT_OFFLOAD_MAX_MB = 500;
|
|
13
|
+
export declare function getOffloadTtlMs(): number;
|
|
14
|
+
export declare function getOffloadMaxBytes(): number;
|
|
15
|
+
export interface OffloadSweepResult {
|
|
16
|
+
removedSessions: number;
|
|
17
|
+
removedBytes: number;
|
|
18
|
+
}
|
|
19
|
+
/** Remove stale/over-budget offload directories. Never throws. */
|
|
20
|
+
export declare function sweepOffloads(root?: string): OffloadSweepResult;
|
|
21
|
+
//# sourceMappingURL=offload-gc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offload-gc.d.ts","sourceRoot":"","sources":["../../src/server/offload-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAsBD,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAoD/D"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage collection for large-result offload files.
|
|
3
|
+
*
|
|
4
|
+
* Offloads live under `<configDir>/offloads/<sessionId>/*.log`. This sweep
|
|
5
|
+
* removes per-session directories older than `SPECTRAL_OFFLOAD_TTL_DAYS`
|
|
6
|
+
* (default 7) and, when the total still exceeds `SPECTRAL_OFFLOAD_MAX_MB`
|
|
7
|
+
* (default 500), evicts the oldest sessions first.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort and synchronous: callers fire-and-forget it during preflight.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { getConfigDir } from "../config.js";
|
|
14
|
+
import { envPositiveInt } from "../sdk/coding-agent/core/tools/offload.js";
|
|
15
|
+
export const DEFAULT_OFFLOAD_TTL_DAYS = 7;
|
|
16
|
+
export const DEFAULT_OFFLOAD_MAX_MB = 500;
|
|
17
|
+
export function getOffloadTtlMs() {
|
|
18
|
+
const days = envPositiveInt("SPECTRAL_OFFLOAD_TTL_DAYS", DEFAULT_OFFLOAD_TTL_DAYS);
|
|
19
|
+
return days * 24 * 60 * 60 * 1000;
|
|
20
|
+
}
|
|
21
|
+
export function getOffloadMaxBytes() {
|
|
22
|
+
const mb = envPositiveInt("SPECTRAL_OFFLOAD_MAX_MB", DEFAULT_OFFLOAD_MAX_MB);
|
|
23
|
+
return mb * 1024 * 1024;
|
|
24
|
+
}
|
|
25
|
+
function dirSizeBytes(dir) {
|
|
26
|
+
let total = 0;
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const full = join(dir, entry.name);
|
|
36
|
+
try {
|
|
37
|
+
if (entry.isDirectory())
|
|
38
|
+
total += dirSizeBytes(full);
|
|
39
|
+
else if (entry.isFile())
|
|
40
|
+
total += statSync(full).size;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Skip unreadable entries — GC is best-effort.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return total;
|
|
47
|
+
}
|
|
48
|
+
/** Remove stale/over-budget offload directories. Never throws. */
|
|
49
|
+
export function sweepOffloads(root) {
|
|
50
|
+
const result = { removedSessions: 0, removedBytes: 0 };
|
|
51
|
+
let base;
|
|
52
|
+
try {
|
|
53
|
+
base = root ?? join(getConfigDir(), "offloads");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
if (!existsSync(base))
|
|
59
|
+
return result;
|
|
60
|
+
let sessionDirs;
|
|
61
|
+
try {
|
|
62
|
+
sessionDirs = readdirSync(base);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
const ttlMs = getOffloadTtlMs();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const removeDir = (dir) => {
|
|
70
|
+
try {
|
|
71
|
+
result.removedBytes += dirSizeBytes(dir);
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
result.removedSessions++;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best-effort.
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// Pass 1: TTL expiry by directory mtime.
|
|
80
|
+
const survivors = [];
|
|
81
|
+
for (const name of sessionDirs) {
|
|
82
|
+
const dir = join(base, name);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = statSync(dir);
|
|
86
|
+
if (!stat.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (now - stat.mtimeMs > ttlMs)
|
|
93
|
+
removeDir(dir);
|
|
94
|
+
else
|
|
95
|
+
survivors.push({ dir, mtime: stat.mtimeMs, size: dirSizeBytes(dir) });
|
|
96
|
+
}
|
|
97
|
+
// Pass 2: total-size budget, oldest first.
|
|
98
|
+
let total = survivors.reduce((sum, s) => sum + s.size, 0);
|
|
99
|
+
const maxBytes = getOffloadMaxBytes();
|
|
100
|
+
if (total <= maxBytes)
|
|
101
|
+
return result;
|
|
102
|
+
survivors.sort((a, b) => a.mtime - b.mtime);
|
|
103
|
+
for (const s of survivors) {
|
|
104
|
+
if (total <= maxBytes)
|
|
105
|
+
break;
|
|
106
|
+
const before = s.size;
|
|
107
|
+
removeDir(s.dir);
|
|
108
|
+
total -= before;
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|