@mono-agent/agent-runtime 0.20.11 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +288 -26
- package/README.md +352 -477
- package/package.json +13 -44
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +108 -9
- package/src/agent/tools/bash.js +11 -26
- package/src/agent/tools/codex-subscription-search.js +123 -29
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +149 -0
- package/src/agent/tools/pi-bridge.js +123 -19
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +293 -0
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +26 -6
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/cost.js +13 -68
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +5 -17
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +221 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +94 -42
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +376 -0
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +38 -14
- package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
- package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +279 -28
- package/src/ai/providers/pi-native.js +206 -61
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +76 -417
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +37 -112
- package/src/index.js +0 -6
- package/src/runtime.js +29 -16
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -2
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +7 -3
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +47 -0
- package/types/agent/tools/pi-bridge.d.ts +7 -4
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +98 -0
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +14 -4
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +2 -4
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +81 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +162 -256
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import { withWebDeadline, coordinatedWebRequest, webRequestFailure } from "./web-request.js";
|
|
1
2
|
// @ts-check
|
|
2
3
|
|
|
3
|
-
import { Readability } from "@mozilla/readability";
|
|
4
|
-
import { Defuddle as parseDefuddle } from "defuddle/node";
|
|
5
|
-
import { DOMParser, parseHTML } from "linkedom";
|
|
6
|
-
import { extractText as extractPdfText, getDocumentProxy } from "unpdf";
|
|
7
4
|
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
8
5
|
import { DEFAULT_MAX_TOOL_OUTPUT_CHARS } from "./shared/constants.js";
|
|
9
6
|
import { capChars } from "./shared/output-truncation.js";
|
|
10
7
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
11
8
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
12
9
|
import { renderWithAgentBrowser } from "./web-browser-render.js";
|
|
10
|
+
import { contentKind, decodeWebBytes, extractWebDocument, markdownToText, shouldAutoRender } from "./web-document-extractor.js";
|
|
11
|
+
import { assertNoWebAccessInterstitial } from "./web-access-interstitial.js";
|
|
13
12
|
|
|
14
13
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
15
14
|
const MAX_REDIRECTS = 5;
|
|
@@ -28,22 +27,23 @@ class WebFetchError extends Error {
|
|
|
28
27
|
/**
|
|
29
28
|
* @param {string} code
|
|
30
29
|
* @param {string} message
|
|
31
|
-
* @param {{retryable?: boolean, statusCode?: number}} [options]
|
|
30
|
+
* @param {{retryable?: boolean, statusCode?: number, retryAfterMs?: number}} [options]
|
|
32
31
|
*/
|
|
33
|
-
constructor(code, message, { retryable = false, statusCode } = {}) {
|
|
32
|
+
constructor(code, message, { retryable = false, statusCode, retryAfterMs } = {}) {
|
|
34
33
|
super(message);
|
|
35
34
|
this.name = "WebFetchError";
|
|
36
35
|
this.code = code;
|
|
37
36
|
this.retryable = retryable;
|
|
38
37
|
this.statusCode = statusCode;
|
|
38
|
+
this.retryAfterMs = retryAfterMs;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Compatibility wrapper for direct callers.
|
|
44
44
|
*
|
|
45
|
-
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number, format?: string, render?: string}} params
|
|
46
|
-
* @param {{sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, retryDelaysMs?: number[], fetchConfig?: any, fetchImpl?: typeof fetch, browserRenderer?: typeof renderWithAgentBrowser, namespace?: string, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
|
|
45
|
+
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number, format?: string, render?: string, start_line?: number, max_lines?: number}} params
|
|
46
|
+
* @param {{documentOnly?: boolean, coordinator?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, retryDelaysMs?: number[], fetchConfig?: any, fetchImpl?: typeof fetch, browserRenderer?: typeof renderWithAgentBrowser, namespace?: string, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
|
|
47
47
|
*/
|
|
48
48
|
export async function webFetchToolImpl(params, options = {}) {
|
|
49
49
|
return (await performWebFetch(params, options)).text;
|
|
@@ -52,18 +52,41 @@ export async function webFetchToolImpl(params, options = {}) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Fetch and locally extract one public URL.
|
|
54
54
|
*
|
|
55
|
-
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number, format?: string, render?: string}} params
|
|
56
|
-
* @param {{sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, retryDelaysMs?: number[], fetchConfig?: any, fetchImpl?: typeof fetch, browserRenderer?: typeof renderWithAgentBrowser, namespace?: string, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
|
|
55
|
+
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number, format?: string, render?: string, start_line?: number, max_lines?: number}} params
|
|
56
|
+
* @param {{documentOnly?: boolean, coordinator?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, retryDelaysMs?: number[], fetchConfig?: any, fetchImpl?: typeof fetch, browserRenderer?: typeof renderWithAgentBrowser, namespace?: string, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
|
|
57
57
|
*/
|
|
58
|
-
export async function performWebFetch(
|
|
58
|
+
export async function performWebFetch(params, options = {}) {
|
|
59
|
+
const started = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
return await withWebDeadline(options.signal, 45_000, async (signal) => {
|
|
62
|
+
const result = await performFetch(params, { ...options, signal });
|
|
63
|
+
if (signal.aborted && !result.error) return failure("Error: WebFetch was aborted or exceeded its deadline.", signal.reason?.code === "deadline_exceeded" ? "deadline_exceeded" : "aborted", started);
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const normalized = webRequestFailure(error, "http", options.signal);
|
|
68
|
+
return failure(`Error: ${normalized.message}`, normalized.code, started, { retryAfterMs: normalized.retryAfterMs });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch and locally extract one public URL.
|
|
74
|
+
*
|
|
75
|
+
* @param {{url: string, headers?: Record<string, string>, max_output_chars?: number, format?: string, render?: string, start_line?: number, max_lines?: number}} params
|
|
76
|
+
* @param {{documentOnly?: boolean, coordinator?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, retryDelaysMs?: number[], fetchConfig?: any, fetchImpl?: typeof fetch, browserRenderer?: typeof renderWithAgentBrowser, namespace?: string, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
|
|
77
|
+
*/
|
|
78
|
+
async function performFetch(
|
|
59
79
|
{
|
|
60
80
|
url,
|
|
61
81
|
headers = {},
|
|
62
82
|
max_output_chars,
|
|
63
83
|
format = "markdown",
|
|
64
84
|
render,
|
|
85
|
+
start_line, max_lines,
|
|
65
86
|
},
|
|
66
87
|
{
|
|
88
|
+
coordinator,
|
|
89
|
+
documentOnly = false,
|
|
67
90
|
sandboxPolicy,
|
|
68
91
|
sandboxEngine,
|
|
69
92
|
ctx,
|
|
@@ -77,6 +100,10 @@ export async function performWebFetch(
|
|
|
77
100
|
} = {},
|
|
78
101
|
) {
|
|
79
102
|
const startedAt = Date.now();
|
|
103
|
+
if ((start_line !== undefined && (!Number.isSafeInteger(start_line) || start_line < 1))
|
|
104
|
+
|| (max_lines !== undefined && (!Number.isSafeInteger(max_lines) || max_lines < 1 || max_lines > 10_000))) {
|
|
105
|
+
return failure("Error: start_line must be positive; max_lines must be between 1 and 10000.", "invalid_range", startedAt);
|
|
106
|
+
}
|
|
80
107
|
let parsed;
|
|
81
108
|
try { parsed = new URL(url); } catch {
|
|
82
109
|
return failure("Error: Invalid URL", "invalid_url", startedAt);
|
|
@@ -115,6 +142,52 @@ export async function performWebFetch(
|
|
|
115
142
|
const resolvedCtx = ctx ?? readToolRuntime();
|
|
116
143
|
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
117
144
|
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
145
|
+
if (requestedRender === "always") {
|
|
146
|
+
if (!sandbox.networkAllowsUrl(policy, parsed.href)) {
|
|
147
|
+
return failure("Error: Network access denied by sandbox policy.", "network_denied", startedAt, {
|
|
148
|
+
backend: "agent-browser", browserRecommended: true, renderReason: "explicit",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const renderedResult = await coordinatedWebRequest(coordinator, "fetch", parsed.origin, signal, async () => ({
|
|
153
|
+
ok: true,
|
|
154
|
+
rendered: await browserRenderer(parsed.href, {
|
|
155
|
+
browserCommand: fetchSettings.browserCommand,
|
|
156
|
+
namespace,
|
|
157
|
+
sandboxPolicy: policy,
|
|
158
|
+
sandboxEngine,
|
|
159
|
+
ctx: resolvedCtx,
|
|
160
|
+
signal,
|
|
161
|
+
registerCleanup,
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
const rendered = normalizeBrowserResult(renderedResult.rendered, parsed.href);
|
|
165
|
+
assertNoWebAccessInterstitial({ url: rendered.finalUrl, text: rendered.text });
|
|
166
|
+
const renderedBody = outputFormat === "text" ? markdownToText(rendered.text) : rendered.text;
|
|
167
|
+
const document = {
|
|
168
|
+
body: renderedBody,
|
|
169
|
+
finalUrl: rendered.finalUrl,
|
|
170
|
+
outcome: {
|
|
171
|
+
status: "ok", code: "ok", retryable: false, attempts: 1,
|
|
172
|
+
backend: "agent-browser", cacheHit: false, durationMs: Date.now() - startedAt,
|
|
173
|
+
bytes: Buffer.byteLength(rendered.text, "utf8"), queueWaitMs: renderedResult.coordinationWaitMs,
|
|
174
|
+
backendDurationMs: renderedResult.backendDurationMs, truncated: renderedBody.length > maxChars,
|
|
175
|
+
redirectCount: 0, rendered: true, renderFailed: false, browserRecommended: false,
|
|
176
|
+
renderReason: "explicit", contentKind: "html", extractionStage: "browser", parserFailureCount: 0,
|
|
177
|
+
parserFailures: [],
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
return documentOnly ? { text: "", error: false, outcome: document.outcome, document }
|
|
181
|
+
: formatWebFetchDocument(document, { start_line, max_lines, max_output_chars: maxChars }, resolvedCtx);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const code = ["access_challenge", "authentication_required", "network_denied"].includes(error?.code)
|
|
184
|
+
? error.code : "browser_render_failed";
|
|
185
|
+
return failure(`Error rendering URL: ${error?.message || String(error)}`, code, startedAt, {
|
|
186
|
+
attempts: 1, backend: "agent-browser", rendered: false, renderFailed: true,
|
|
187
|
+
browserRecommended: code === "browser_render_failed", renderReason: "explicit",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
118
191
|
const delays = Array.isArray(retryDelaysMs)
|
|
119
192
|
? retryDelaysMs.slice(0, 2).map((value) => Math.max(0, Number(value) || 0))
|
|
120
193
|
: [];
|
|
@@ -123,18 +196,23 @@ export async function performWebFetch(
|
|
|
123
196
|
let finalUrl = parsed.href;
|
|
124
197
|
let redirectCount = 0;
|
|
125
198
|
let responseBytes = 0;
|
|
199
|
+
let queueWaitMs = 0;
|
|
200
|
+
let backendDurationMs = 0;
|
|
126
201
|
let bytes;
|
|
127
202
|
|
|
128
203
|
for (let attempt = 0; attempt <= delays.length; attempt += 1) {
|
|
129
204
|
attempts += 1;
|
|
130
205
|
try {
|
|
131
206
|
const fetched = await fetchFollowingRedirects(parsed, {
|
|
207
|
+
coordinator,
|
|
132
208
|
headers: requestHeaders.headers,
|
|
133
209
|
sandbox,
|
|
134
210
|
policy,
|
|
135
211
|
signal,
|
|
136
212
|
fetchImpl,
|
|
137
213
|
});
|
|
214
|
+
queueWaitMs += fetched.queueWaitMs;
|
|
215
|
+
backendDurationMs += fetched.backendDurationMs;
|
|
138
216
|
response = fetched.response;
|
|
139
217
|
finalUrl = fetched.url;
|
|
140
218
|
redirectCount = fetched.redirects;
|
|
@@ -144,12 +222,12 @@ export async function performWebFetch(
|
|
|
144
222
|
await waitForRetry(delay, signal);
|
|
145
223
|
continue;
|
|
146
224
|
}
|
|
147
|
-
bytes =
|
|
225
|
+
bytes = fetched.bytes;
|
|
148
226
|
responseBytes = bytes.byteLength;
|
|
149
227
|
break;
|
|
150
228
|
} catch (error) {
|
|
151
229
|
if (signal?.aborted) {
|
|
152
|
-
return failure("Error fetching URL: request aborted", "aborted", startedAt, {
|
|
230
|
+
return failure("Error fetching URL: request aborted", signal.reason?.code === "deadline_exceeded" ? "deadline_exceeded" : "aborted", startedAt, {
|
|
153
231
|
attempts,
|
|
154
232
|
retryable: false,
|
|
155
233
|
});
|
|
@@ -176,6 +254,9 @@ export async function performWebFetch(
|
|
|
176
254
|
attempts,
|
|
177
255
|
retryable: normalized.retryable,
|
|
178
256
|
statusCode: normalized.statusCode ?? response?.status,
|
|
257
|
+
retryAfterMs: normalized.retryAfterMs, queueWaitMs, backendDurationMs,
|
|
258
|
+
browserRecommended: requestedRender === "auto"
|
|
259
|
+
&& !["network_denied", "redirect_network_denied", "aborted", "deadline_exceeded"].includes(normalized.code),
|
|
179
260
|
});
|
|
180
261
|
}
|
|
181
262
|
}
|
|
@@ -186,10 +267,46 @@ export async function performWebFetch(
|
|
|
186
267
|
|
|
187
268
|
const contentType = response.headers.get("content-type") || "";
|
|
188
269
|
const responseKind = contentKind(contentType, bytes);
|
|
270
|
+
let decodedForExtraction;
|
|
271
|
+
if (responseKind !== "binary") {
|
|
272
|
+
try {
|
|
273
|
+
decodedForExtraction = decodeWebBytes(bytes, contentType, responseKind);
|
|
274
|
+
assertNoWebAccessInterstitial({
|
|
275
|
+
url: finalUrl,
|
|
276
|
+
text: decodedForExtraction.text,
|
|
277
|
+
statusCode: response.status,
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (["access_challenge", "authentication_required"].includes(error?.code)) {
|
|
281
|
+
return failure(`Error fetching URL: ${error.message}`, error.code, startedAt, {
|
|
282
|
+
attempts,
|
|
283
|
+
statusCode: response.status,
|
|
284
|
+
bytes: responseBytes,
|
|
285
|
+
backend: "http",
|
|
286
|
+
redirectCount,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Unsupported encodings are still handled by the extraction path below,
|
|
290
|
+
// where their decoding metadata and browser recommendation are retained.
|
|
291
|
+
decodedForExtraction = undefined;
|
|
292
|
+
}
|
|
293
|
+
} else if ([401, 407].includes(response.status)) {
|
|
294
|
+
try {
|
|
295
|
+
assertNoWebAccessInterstitial({ url: finalUrl, statusCode: response.status });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return failure(`Error fetching URL: ${error.message}`, error.code, startedAt, {
|
|
298
|
+
attempts,
|
|
299
|
+
statusCode: response.status,
|
|
300
|
+
bytes: responseBytes,
|
|
301
|
+
backend: "http",
|
|
302
|
+
redirectCount,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
189
306
|
if (!response.ok) {
|
|
190
307
|
const preview = responseKind === "binary"
|
|
191
308
|
? "(binary response body omitted)"
|
|
192
|
-
:
|
|
309
|
+
: safeDecodePreview(bytes, contentType, responseKind);
|
|
193
310
|
const errorText = [
|
|
194
311
|
`HTTP ${response.status}`,
|
|
195
312
|
`[BEGIN UNTRUSTED WEB ERROR BODY source=${JSON.stringify(finalUrl)}]`,
|
|
@@ -203,6 +320,7 @@ export async function performWebFetch(
|
|
|
203
320
|
bytes: responseBytes,
|
|
204
321
|
backend: "http",
|
|
205
322
|
redirectCount,
|
|
323
|
+
browserRecommended: requestedRender === "auto" && [406, 415].includes(response.status),
|
|
206
324
|
});
|
|
207
325
|
}
|
|
208
326
|
if (responseKind === "binary") {
|
|
@@ -216,14 +334,40 @@ export async function performWebFetch(
|
|
|
216
334
|
}
|
|
217
335
|
|
|
218
336
|
let extracted;
|
|
337
|
+
let decoding;
|
|
219
338
|
try {
|
|
220
|
-
|
|
339
|
+
decoding = decodedForExtraction ?? decodeWebBytes(bytes, contentType, responseKind);
|
|
340
|
+
extracted = await extractWebDocument(bytes, {
|
|
221
341
|
contentType,
|
|
222
342
|
format: outputFormat,
|
|
223
343
|
url: finalUrl,
|
|
224
344
|
});
|
|
225
345
|
} catch (error) {
|
|
226
|
-
return failure(`Error extracting URL: ${error?.message || String(error)}`, "extraction_failed", startedAt, {
|
|
346
|
+
return failure(`Error extracting URL: ${error?.message || String(error)}`, error?.code || "extraction_failed", startedAt, {
|
|
347
|
+
attempts,
|
|
348
|
+
statusCode: response.status,
|
|
349
|
+
bytes: responseBytes,
|
|
350
|
+
backend: "http",
|
|
351
|
+
redirectCount,
|
|
352
|
+
browserRecommended: requestedRender === "auto" && responseKind === "html",
|
|
353
|
+
contentKind: responseKind,
|
|
354
|
+
...(decoding === undefined ? {} : {
|
|
355
|
+
charset: decoding.charset,
|
|
356
|
+
charsetSource: decoding.charsetSource,
|
|
357
|
+
hadDecodingReplacement: decoding.hadDecodingReplacement,
|
|
358
|
+
}),
|
|
359
|
+
...(Array.isArray(error?.parserFailures) ? { parserFailures: error.parserFailures.slice(0, 3) } : {}),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
assertNoWebAccessInterstitial({
|
|
365
|
+
url: finalUrl,
|
|
366
|
+
text: extracted.readableText,
|
|
367
|
+
statusCode: response.status,
|
|
368
|
+
});
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return failure(`Error fetching URL: ${error.message}`, error.code, startedAt, {
|
|
227
371
|
attempts,
|
|
228
372
|
statusCode: response.status,
|
|
229
373
|
bytes: responseBytes,
|
|
@@ -235,13 +379,13 @@ export async function performWebFetch(
|
|
|
235
379
|
const shouldRender = responseKind === "html"
|
|
236
380
|
&& (
|
|
237
381
|
requestedRender === "always"
|
|
238
|
-
|| (requestedRender === "auto" && shouldAutoRender(extracted.readableText,
|
|
382
|
+
|| (requestedRender === "auto" && shouldAutoRender(extracted.readableText, decodedText(bytes, contentType, responseKind)))
|
|
239
383
|
);
|
|
240
384
|
let backend = "http";
|
|
241
385
|
let renderFailed = false;
|
|
242
386
|
if (shouldRender) {
|
|
243
387
|
try {
|
|
244
|
-
const
|
|
388
|
+
const renderedResult = await coordinatedWebRequest(coordinator, "fetch", new URL(finalUrl).origin, signal, async () => ({ ok: true, rendered: await browserRenderer(finalUrl, {
|
|
245
389
|
browserCommand: fetchSettings.browserCommand,
|
|
246
390
|
namespace,
|
|
247
391
|
sandboxPolicy,
|
|
@@ -249,16 +393,32 @@ export async function performWebFetch(
|
|
|
249
393
|
ctx: resolvedCtx,
|
|
250
394
|
signal,
|
|
251
395
|
registerCleanup,
|
|
252
|
-
});
|
|
396
|
+
}) }));
|
|
397
|
+
queueWaitMs += renderedResult.coordinationWaitMs;
|
|
398
|
+
backendDurationMs += renderedResult.backendDurationMs;
|
|
399
|
+
const rendered = normalizeBrowserResult(renderedResult.rendered, finalUrl);
|
|
400
|
+
assertNoWebAccessInterstitial({ url: rendered.finalUrl, text: rendered.text });
|
|
401
|
+
signal?.throwIfAborted();
|
|
253
402
|
extracted = {
|
|
254
|
-
body: outputFormat === "text" ? markdownToText(rendered) : rendered,
|
|
255
|
-
readableText: markdownToText(rendered),
|
|
403
|
+
body: outputFormat === "text" ? markdownToText(rendered.text) : rendered.text,
|
|
404
|
+
readableText: markdownToText(rendered.text),
|
|
256
405
|
title: extracted.title,
|
|
406
|
+
charset: extracted.charset,
|
|
407
|
+
charsetSource: extracted.charsetSource,
|
|
408
|
+
hadDecodingReplacement: extracted.hadDecodingReplacement,
|
|
409
|
+
extractionStage: "browser",
|
|
410
|
+
parserFailureCount: extracted.parserFailureCount,
|
|
411
|
+
parserFailures: extracted.parserFailures,
|
|
257
412
|
};
|
|
413
|
+
finalUrl = rendered.finalUrl;
|
|
258
414
|
backend = "agent-browser";
|
|
259
415
|
} catch (error) {
|
|
260
|
-
if (
|
|
261
|
-
|
|
416
|
+
if (signal?.aborted) return failure("Error: WebFetch rendering was aborted.", "aborted", startedAt);
|
|
417
|
+
const terminalCode = ["access_challenge", "authentication_required", "network_denied"].includes(error?.code)
|
|
418
|
+
? error.code : null;
|
|
419
|
+
if (terminalCode || requestedRender === "always" || error?.code === "coordination_unavailable") {
|
|
420
|
+
const code = terminalCode ?? "browser_render_failed";
|
|
421
|
+
return failure(`Error rendering URL: ${error?.message || String(error)}`, code, startedAt, {
|
|
262
422
|
attempts,
|
|
263
423
|
statusCode: response.status,
|
|
264
424
|
bytes: responseBytes,
|
|
@@ -270,15 +430,11 @@ export async function performWebFetch(
|
|
|
270
430
|
}
|
|
271
431
|
}
|
|
272
432
|
|
|
433
|
+
if (responseKind === "html" && backend === "http" && shouldAutoRender(extracted.readableText, decodedText(bytes, contentType, responseKind))) {
|
|
434
|
+
return failure("Error: Page contains an unusable loading shell; no readable evidence was retrieved.", "unusable_content", startedAt, { backend, rendered: false, renderFailed, browserRecommended: true });
|
|
435
|
+
}
|
|
273
436
|
const body = extracted.body || "(no readable content)";
|
|
274
|
-
const
|
|
275
|
-
const text = [
|
|
276
|
-
`[BEGIN UNTRUSTED WEB CONTENT source=${JSON.stringify(finalUrl)}]`,
|
|
277
|
-
capped,
|
|
278
|
-
"[END UNTRUSTED WEB CONTENT]",
|
|
279
|
-
].join("\n");
|
|
280
|
-
return {
|
|
281
|
-
text,
|
|
437
|
+
const document = { body, finalUrl,
|
|
282
438
|
outcome: {
|
|
283
439
|
status: "ok",
|
|
284
440
|
code: renderFailed ? "ok_static_render_failed" : "ok",
|
|
@@ -288,19 +444,37 @@ export async function performWebFetch(
|
|
|
288
444
|
cacheHit: false,
|
|
289
445
|
durationMs: Date.now() - startedAt,
|
|
290
446
|
bytes: responseBytes,
|
|
447
|
+
queueWaitMs, backendDurationMs,
|
|
291
448
|
truncated: body.length > maxChars,
|
|
292
449
|
statusCode: response.status,
|
|
293
450
|
redirectCount,
|
|
294
451
|
rendered: backend === "agent-browser",
|
|
295
452
|
renderFailed,
|
|
453
|
+
browserRecommended: renderFailed,
|
|
454
|
+
...(backend === "agent-browser" ? { renderReason: "sparse_html" } : {}),
|
|
296
455
|
contentKind: responseKind,
|
|
456
|
+
charset: extracted.charset,
|
|
457
|
+
charsetSource: extracted.charsetSource,
|
|
458
|
+
hadDecodingReplacement: extracted.hadDecodingReplacement,
|
|
459
|
+
extractionStage: extracted.extractionStage,
|
|
460
|
+
parserFailureCount: extracted.parserFailureCount ?? 0,
|
|
461
|
+
parserFailures: extracted.parserFailures ?? [],
|
|
297
462
|
},
|
|
298
|
-
|
|
299
|
-
}
|
|
463
|
+
};
|
|
464
|
+
return documentOnly ? { text: "", error: false, outcome: document.outcome, document }
|
|
465
|
+
: formatWebFetchDocument(document, { start_line, max_lines, max_output_chars: maxChars }, resolvedCtx);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function normalizeBrowserResult(value, requestedUrl) {
|
|
469
|
+
if (typeof value === "string") return { text: value, finalUrl: requestedUrl };
|
|
470
|
+
if (value && typeof value.text === "string" && typeof value.finalUrl === "string") return value;
|
|
471
|
+
throw Object.assign(new Error("Browser renderer returned an invalid result."), { code: "browser_render_failed" });
|
|
300
472
|
}
|
|
301
473
|
|
|
302
474
|
async function fetchFollowingRedirects(initialUrl, options) {
|
|
303
475
|
let current = new URL(initialUrl.href);
|
|
476
|
+
let queueWaitMs = 0;
|
|
477
|
+
let backendDurationMs = 0;
|
|
304
478
|
for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
|
|
305
479
|
if (!options.sandbox.networkAllowsUrl(options.policy, current.href)) {
|
|
306
480
|
throw new WebFetchError(
|
|
@@ -310,14 +484,25 @@ async function fetchFollowingRedirects(initialUrl, options) {
|
|
|
310
484
|
: "Network access denied by sandbox policy (redirect).",
|
|
311
485
|
);
|
|
312
486
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
487
|
+
const fetched = await coordinatedWebRequest(options.coordinator, "fetch", current.origin, options.signal, async () => {
|
|
488
|
+
const response = await options.fetchImpl(current, {
|
|
489
|
+
headers: options.headers, redirect: "manual", signal: requestSignal(options.signal),
|
|
490
|
+
});
|
|
491
|
+
const redirect = response.status >= 300 && response.status < 400 && response.headers.has("location");
|
|
492
|
+
let bytes;
|
|
493
|
+
if (redirect) { await response.body?.cancel(); bytes = new Uint8Array(); }
|
|
494
|
+
else bytes = await readResponseBytes(response);
|
|
495
|
+
return { response, bytes };
|
|
496
|
+
}, ({ response }) => ({
|
|
497
|
+
status: response.status === 429 ? "rate_limited" : response.status >= 500 ? "unavailable" : "ok",
|
|
498
|
+
...(response.status === 429 ? { retryAfterMs: retryAfterMilliseconds(response) } : {}),
|
|
499
|
+
}));
|
|
500
|
+
queueWaitMs += fetched.coordinationWaitMs;
|
|
501
|
+
backendDurationMs += fetched.backendDurationMs;
|
|
502
|
+
const { response, bytes } = fetched;
|
|
318
503
|
const location = response.headers.get("location");
|
|
319
504
|
if (response.status < 300 || response.status >= 400 || !location) {
|
|
320
|
-
return { response, url: current.href, redirects: hop };
|
|
505
|
+
return { response, bytes, url: current.href, redirects: hop, queueWaitMs, backendDurationMs };
|
|
321
506
|
}
|
|
322
507
|
if (hop === MAX_REDIRECTS) {
|
|
323
508
|
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
@@ -368,166 +553,13 @@ async function readResponseBytes(response) {
|
|
|
368
553
|
return new Uint8Array(Buffer.concat(chunks));
|
|
369
554
|
}
|
|
370
555
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const raw = decodeBytes(bytes, contentType);
|
|
374
|
-
if (format === "raw") return { body: raw, readableText: raw, title: "" };
|
|
375
|
-
if (kind === "pdf") {
|
|
376
|
-
const pdf = await getDocumentProxy(bytes);
|
|
377
|
-
try {
|
|
378
|
-
const extracted = await extractPdfText(pdf, { mergePages: true });
|
|
379
|
-
const body = String(extracted.text || "").trim();
|
|
380
|
-
return {
|
|
381
|
-
body,
|
|
382
|
-
readableText: body,
|
|
383
|
-
title: "",
|
|
384
|
-
};
|
|
385
|
-
} finally {
|
|
386
|
-
try { await /** @type {any} */ (pdf).destroy?.(); } catch { /* best effort */ }
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (kind === "json") {
|
|
390
|
-
let parsed;
|
|
391
|
-
try { parsed = JSON.parse(raw); } catch {
|
|
392
|
-
return { body: raw, readableText: raw, title: "" };
|
|
393
|
-
}
|
|
394
|
-
const pretty = JSON.stringify(parsed, null, 2);
|
|
395
|
-
return {
|
|
396
|
-
body: format === "markdown" ? `\`\`\`json\n${pretty}\n\`\`\`` : pretty,
|
|
397
|
-
readableText: pretty,
|
|
398
|
-
title: "",
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
if (kind === "xml") {
|
|
402
|
-
const body = extractXml(raw, format);
|
|
403
|
-
return { body, readableText: markdownToText(body), title: "" };
|
|
404
|
-
}
|
|
405
|
-
if (kind === "html") {
|
|
406
|
-
return extractHtml(raw, url, format);
|
|
407
|
-
}
|
|
408
|
-
return { body: raw.trim(), readableText: raw.trim(), title: "" };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function extractHtml(html, url, format) {
|
|
412
|
-
let title = "";
|
|
413
|
-
let markdown = "";
|
|
414
|
-
try {
|
|
415
|
-
const { document } = parseHTML(html);
|
|
416
|
-
const parsed = await parseDefuddle(/** @type {any} */ (document), url, {
|
|
417
|
-
markdown: true,
|
|
418
|
-
separateMarkdown: true,
|
|
419
|
-
useAsync: false,
|
|
420
|
-
});
|
|
421
|
-
title = String(parsed.title || "").trim();
|
|
422
|
-
markdown = String(parsed.contentMarkdown || parsed.content || "").trim();
|
|
423
|
-
} catch { /* Readability fallback below */ }
|
|
424
|
-
|
|
425
|
-
if (meaningfulCharacters(markdown) < 1) {
|
|
426
|
-
try {
|
|
427
|
-
const { document } = parseHTML(html);
|
|
428
|
-
const article = new Readability(/** @type {any} */ (document)).parse();
|
|
429
|
-
if (article) {
|
|
430
|
-
title ||= String(article.title || "").trim();
|
|
431
|
-
markdown = htmlToText(article.content || article.textContent || "");
|
|
432
|
-
}
|
|
433
|
-
} catch { /* final body-text fallback below */ }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (meaningfulCharacters(markdown) < 1) {
|
|
437
|
-
const { document } = parseHTML(html);
|
|
438
|
-
title ||= collapseWhitespace(document.querySelector("title")?.textContent);
|
|
439
|
-
markdown = collapseDocumentText(document.body?.textContent || "");
|
|
440
|
-
}
|
|
441
|
-
const readableText = markdownToText(markdown);
|
|
442
|
-
if (format === "text") return { body: readableText, readableText, title };
|
|
443
|
-
const body = title && !markdown.trimStart().startsWith(`# ${title}`)
|
|
444
|
-
? `# ${title}\n\n${markdown}`
|
|
445
|
-
: markdown;
|
|
446
|
-
return { body, readableText, title };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function extractXml(xml, format) {
|
|
450
|
-
const document = new DOMParser().parseFromString(String(xml || ""), "text/xml");
|
|
451
|
-
const entries = [...document.querySelectorAll("item, entry")].slice(0, 50);
|
|
452
|
-
if (entries.length === 0) {
|
|
453
|
-
return collapseDocumentText(document.documentElement?.textContent || xml);
|
|
454
|
-
}
|
|
455
|
-
const blocks = entries.map((entry) => {
|
|
456
|
-
const title = collapseWhitespace(entry.querySelector("title")?.textContent) || "Untitled";
|
|
457
|
-
const linkElement = entry.querySelector("link");
|
|
458
|
-
const link = linkElement?.getAttribute("href") || collapseWhitespace(linkElement?.textContent);
|
|
459
|
-
const description = collapseWhitespace(
|
|
460
|
-
entry.querySelector("description, summary, content")?.textContent,
|
|
461
|
-
);
|
|
462
|
-
if (format === "text") return [title, link, description].filter(Boolean).join("\n");
|
|
463
|
-
return [
|
|
464
|
-
`## ${title}`,
|
|
465
|
-
link ? `[${link}](${link})` : "",
|
|
466
|
-
description,
|
|
467
|
-
].filter(Boolean).join("\n\n");
|
|
468
|
-
});
|
|
469
|
-
return blocks.join("\n\n");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function contentKind(contentType, bytes) {
|
|
473
|
-
const mime = String(contentType || "").split(";", 1)[0].trim().toLowerCase();
|
|
474
|
-
if (mime === "application/pdf" || startsWithPdf(bytes)) return "pdf";
|
|
475
|
-
if (mime.includes("json") || mime.endsWith("+json")) return "json";
|
|
476
|
-
if (
|
|
477
|
-
mime.includes("xml")
|
|
478
|
-
|| mime.includes("rss")
|
|
479
|
-
|| mime.includes("atom")
|
|
480
|
-
|| mime.endsWith("+xml")
|
|
481
|
-
) return "xml";
|
|
482
|
-
if (mime.includes("html") || looksLikeHtml(bytes)) return "html";
|
|
483
|
-
if (
|
|
484
|
-
mime.startsWith("text/")
|
|
485
|
-
|| [
|
|
486
|
-
"application/ecmascript",
|
|
487
|
-
"application/graphql",
|
|
488
|
-
"application/javascript",
|
|
489
|
-
"application/rtf",
|
|
490
|
-
"application/sql",
|
|
491
|
-
"application/x-httpd-php",
|
|
492
|
-
"application/x-yaml",
|
|
493
|
-
"application/yaml",
|
|
494
|
-
].includes(mime)
|
|
495
|
-
) return "text";
|
|
496
|
-
if (!mime && !looksBinary(bytes)) return "text";
|
|
497
|
-
return "binary";
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function startsWithPdf(bytes) {
|
|
501
|
-
return Buffer.from(bytes.subarray(0, 5)).toString("ascii") === "%PDF-";
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function looksLikeHtml(bytes) {
|
|
505
|
-
return /^\s*(?:<!doctype html|<html|<head|<body)/iu.test(
|
|
506
|
-
Buffer.from(bytes.subarray(0, 512)).toString("utf8"),
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function looksBinary(bytes) {
|
|
511
|
-
const sample = bytes.subarray(0, Math.min(bytes.byteLength, 1_024));
|
|
512
|
-
if (sample.byteLength === 0) return false;
|
|
513
|
-
let controls = 0;
|
|
514
|
-
for (const byte of sample) {
|
|
515
|
-
if (byte === 0) return true;
|
|
516
|
-
if (byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0c && byte !== 0x0d) {
|
|
517
|
-
controls += 1;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
return controls / sample.byteLength > 0.1;
|
|
556
|
+
function decodedText(bytes, contentType, kind) {
|
|
557
|
+
return decodeWebBytes(bytes, contentType, kind).text;
|
|
521
558
|
}
|
|
522
559
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
return new TextDecoder(charset, { fatal: false }).decode(bytes);
|
|
528
|
-
} catch {
|
|
529
|
-
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
530
|
-
}
|
|
560
|
+
function safeDecodePreview(bytes, contentType, kind) {
|
|
561
|
+
try { return decodedText(bytes, contentType, kind).slice(0, 500); }
|
|
562
|
+
catch { return new TextDecoder("utf-8", { fatal: false }).decode(bytes).slice(0, 500); }
|
|
531
563
|
}
|
|
532
564
|
|
|
533
565
|
function normalizeRequestHeaders(headers) {
|
|
@@ -568,51 +600,6 @@ function normalizeFetchConfig(input) {
|
|
|
568
600
|
return { render, browserCommand: browserCommand.trim() };
|
|
569
601
|
}
|
|
570
602
|
|
|
571
|
-
function shouldAutoRender(readableText, html) {
|
|
572
|
-
if (meaningfulCharacters(readableText) >= 200) return false;
|
|
573
|
-
const scriptCount = (html.match(/<script\b/giu) || []).length;
|
|
574
|
-
const hasAppRoot = /<(?:div|main)[^>]+(?:id|class)=["'][^"']*(?:app|root|__next|nuxt|svelte)[^"']*["']/iu.test(html);
|
|
575
|
-
const hasSpaAssets = /\b(?:webpack|__NEXT_DATA__|vite|hydration|data-reactroot)\b/iu.test(html);
|
|
576
|
-
return scriptCount >= 2 && (hasAppRoot || hasSpaAssets);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function meaningfulCharacters(value) {
|
|
580
|
-
return markdownToText(value).replace(/\s/gu, "").length;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function htmlToText(value) {
|
|
584
|
-
try {
|
|
585
|
-
const { document } = parseHTML(String(value || ""));
|
|
586
|
-
return collapseDocumentText(document.body?.textContent || document.documentElement?.textContent || "");
|
|
587
|
-
} catch {
|
|
588
|
-
return collapseDocumentText(String(value || "").replace(/<[^>]+>/gu, " "));
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function markdownToText(value) {
|
|
593
|
-
return collapseDocumentText(
|
|
594
|
-
String(value || "")
|
|
595
|
-
.replace(/```[\s\S]*?```/gu, (block) => block.replace(/^```[^\n]*\n?|```$/gu, ""))
|
|
596
|
-
.replace(/!\[([^\]]*)\]\([^)]*\)/gu, "$1")
|
|
597
|
-
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
|
|
598
|
-
.replace(/^[#>*+-]+\s*/gmu, "")
|
|
599
|
-
.replace(/[*_`~]/gu, ""),
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function collapseDocumentText(value) {
|
|
604
|
-
return String(value || "")
|
|
605
|
-
.replace(/\r/gu, "")
|
|
606
|
-
.replace(/[ \t]+\n/gu, "\n")
|
|
607
|
-
.replace(/\n{3,}/gu, "\n\n")
|
|
608
|
-
.replace(/[ \t]{2,}/gu, " ")
|
|
609
|
-
.trim();
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function collapseWhitespace(value) {
|
|
613
|
-
return String(value || "").replace(/\s+/gu, " ").trim();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
603
|
function requestSignal(signal) {
|
|
617
604
|
const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
618
605
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -662,7 +649,7 @@ function normalizeFetchError(error) {
|
|
|
662
649
|
return new WebFetchError(
|
|
663
650
|
timedOut ? "timeout" : (aborted ? "aborted" : (code || "request_failed")),
|
|
664
651
|
error?.message || String(error),
|
|
665
|
-
{ retryable: transient },
|
|
652
|
+
{ retryable: transient, retryAfterMs: error?.retryAfterMs },
|
|
666
653
|
);
|
|
667
654
|
}
|
|
668
655
|
|
|
@@ -689,3 +676,44 @@ function positiveInteger(value, fallback) {
|
|
|
689
676
|
const number = Number(value);
|
|
690
677
|
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
691
678
|
}
|
|
679
|
+
|
|
680
|
+
function retryAfterMilliseconds(response) {
|
|
681
|
+
const raw = response.headers.get("retry-after");
|
|
682
|
+
if (!raw) return undefined;
|
|
683
|
+
const seconds = Number(raw);
|
|
684
|
+
const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw) - Date.now();
|
|
685
|
+
return Number.isFinite(delay) ? Math.max(0, delay) : undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function formatWebFetchDocument(document, params, ctx) {
|
|
689
|
+
if ((params.start_line !== undefined && (!Number.isSafeInteger(params.start_line) || params.start_line < 1))
|
|
690
|
+
|| (params.max_lines !== undefined && (!Number.isSafeInteger(params.max_lines) || params.max_lines < 1 || params.max_lines > 10000))) {
|
|
691
|
+
return failure("Error: Invalid WebFetch line range.", "invalid_range", Date.now());
|
|
692
|
+
}
|
|
693
|
+
const { body, finalUrl } = document;
|
|
694
|
+
const ranged = params.start_line !== undefined || params.max_lines !== undefined;
|
|
695
|
+
const lines = body.split("\n");
|
|
696
|
+
const start = params.start_line ?? 1;
|
|
697
|
+
const count = params.max_lines ?? 200;
|
|
698
|
+
const selected = ranged ? lines.slice(start - 1, start - 1 + count).join("\n") : body;
|
|
699
|
+
const maxChars = positiveInteger(params.max_output_chars, DEFAULT_MAX_TOOL_OUTPUT_CHARS);
|
|
700
|
+
const capped = capChars(selected, { label: "WebFetch", maxChars, ctx });
|
|
701
|
+
const shownLines = capped === selected ? (selected ? selected.split("\n").length : 0)
|
|
702
|
+
: Math.max(0, capped.slice(0, capped.lastIndexOf("[truncated WebFetch output:")).split("\n").length - 1);
|
|
703
|
+
const end = Math.min(lines.length, start - 1 + shownLines);
|
|
704
|
+
const continuation = end < lines.length ? Math.max(start, end + 1) : null;
|
|
705
|
+
const continuationHint = continuation === start && capped !== selected
|
|
706
|
+
? `The next line exceeds the output budget. Increase max_output_chars or read the saved output artifact; repeating this range with the same budget cannot advance.`
|
|
707
|
+
: `Continue with WebFetch url=${JSON.stringify(finalUrl)} start_line=${continuation} max_lines=${count}.`;
|
|
708
|
+
return {
|
|
709
|
+
text: [`[BEGIN UNTRUSTED WEB CONTENT source=${JSON.stringify(finalUrl)}]`,
|
|
710
|
+
...(ranged || continuation ? [`[Lines ${start}-${end} of ${lines.length}.]`] : []),
|
|
711
|
+
capped,
|
|
712
|
+
...(continuation ? [`[${continuationHint}]`] : []),
|
|
713
|
+
"[END UNTRUSTED WEB CONTENT]"].join("\n"),
|
|
714
|
+
outcome: { ...document.outcome, truncated: selected.length > maxChars || end < lines.length,
|
|
715
|
+
startLine: start, endLine: end, totalLines: lines.length, nextLine: continuation },
|
|
716
|
+
error: false,
|
|
717
|
+
document,
|
|
718
|
+
};
|
|
719
|
+
}
|