@mono-agent/agent-runtime 0.20.14 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +30 -7
- package/README.md +219 -35
- package/package.json +9 -4
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +104 -5
- package/src/agent/tools/bash.js +10 -2
- package/src/agent/tools/codex-subscription-search.js +122 -28
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/monitor.js +11 -2
- package/src/agent/tools/pi-bridge.js +70 -26
- package/src/agent/tools/read.js +3 -78
- package/src/agent/tools/shared/image.js +89 -0
- package/src/agent/tools/shared/monitors.js +22 -3
- 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 +3 -1
- 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/failure.js +3 -3
- package/src/ai/index.js +1 -0
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/pi-native/compaction-driver.js +45 -21
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +40 -2
- 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 +28 -4
- package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
- package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +245 -13
- package/src/ai/providers/pi-native.js +159 -40
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/router.js +30 -11
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +26 -5
- package/src/runtime.js +24 -5
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -1
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +6 -2
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/monitor.d.ts +5 -2
- package/types/agent/tools/pi-bridge.d.ts +7 -5
- package/types/agent/tools/shared/image.d.ts +23 -0
- package/types/agent/tools/shared/monitors.d.ts +17 -2
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +3 -2
- 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/index.d.ts +1 -0
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +61 -0
- package/types/ai/provider-check.d.ts +53 -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 +3 -1
- 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 +11 -1
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +140 -12
|
@@ -7,11 +7,37 @@ import { passthroughSandbox } from "../sandbox-seam.js";
|
|
|
7
7
|
import { runPreparedProcess } from "./shared/process-runner.js";
|
|
8
8
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
9
9
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
10
|
+
import { assertNoWebAccessInterstitial } from "./web-access-interstitial.js";
|
|
10
11
|
|
|
11
12
|
const BROWSER_TIMEOUT_MS = 20_000;
|
|
12
13
|
const BROWSER_CLOSE_TIMEOUT_MS = 5_000;
|
|
13
14
|
const BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
14
15
|
const MAX_BROWSER_NAMESPACE_CHARS = 16;
|
|
16
|
+
const BROWSER_HOST_ENV_KEYS = [
|
|
17
|
+
"COMSPEC", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME", "PATH", "PATHEXT",
|
|
18
|
+
"SHELL", "SystemRoot", "TEMP", "TMP", "TMPDIR", "USER", "WINDIR",
|
|
19
|
+
];
|
|
20
|
+
const BLOCKED_BROWSER_ENV_KEYS = [
|
|
21
|
+
"ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
22
|
+
"all_proxy", "http_proxy", "https_proxy", "no_proxy",
|
|
23
|
+
"AGENT_BROWSER_ACTION_POLICY", "AGENT_BROWSER_ALLOWED_DOMAINS", "AGENT_BROWSER_ALLOW_FILE_ACCESS",
|
|
24
|
+
"AGENT_BROWSER_ANNOTATE", "AGENT_BROWSER_ARGS", "AGENT_BROWSER_CDP", "AGENT_BROWSER_COLOR_SCHEME",
|
|
25
|
+
"AGENT_BROWSER_CONFIRM_ACTIONS", "AGENT_BROWSER_CONFIRM_INTERACTIVE", "AGENT_BROWSER_CONTENT_BOUNDARIES",
|
|
26
|
+
"AGENT_BROWSER_DEFAULT_TIMEOUT", "AGENT_BROWSER_DOWNLOAD_PATH", "AGENT_BROWSER_ENABLE",
|
|
27
|
+
"AGENT_BROWSER_ENCRYPTION_KEY", "AGENT_BROWSER_ENGINE", "AGENT_BROWSER_EXECUTABLE_PATH",
|
|
28
|
+
"AGENT_BROWSER_EXTENSIONS", "AGENT_BROWSER_HEADED", "AGENT_BROWSER_HIDE_SCROLLBARS",
|
|
29
|
+
"AGENT_BROWSER_IDLE_TIMEOUT_MS", "AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "AGENT_BROWSER_INIT_SCRIPTS",
|
|
30
|
+
"AGENT_BROWSER_IOS_DEVICE", "AGENT_BROWSER_IOS_UDID", "AGENT_BROWSER_MAX_OUTPUT",
|
|
31
|
+
"AGENT_BROWSER_NAMESPACE", "AGENT_BROWSER_NO_AUTO_DIALOG", "AGENT_BROWSER_NO_XVFB",
|
|
32
|
+
"AGENT_BROWSER_PLUGINS", "AGENT_BROWSER_PROFILE", "AGENT_BROWSER_PROVIDER", "AGENT_BROWSER_PROXY",
|
|
33
|
+
"AGENT_BROWSER_PROXY_BYPASS", "AGENT_BROWSER_RESTORE", "AGENT_BROWSER_RESTORE_CHECK_FN",
|
|
34
|
+
"AGENT_BROWSER_RESTORE_CHECK_TEXT", "AGENT_BROWSER_RESTORE_CHECK_URL", "AGENT_BROWSER_SANDBOX_VERSION",
|
|
35
|
+
"AGENT_BROWSER_SCREENSHOT_DIR", "AGENT_BROWSER_SCREENSHOT_FORMAT", "AGENT_BROWSER_SCREENSHOT_QUALITY",
|
|
36
|
+
"AGENT_BROWSER_SESSION", "AGENT_BROWSER_SESSION_NAME", "AGENT_BROWSER_SKILLS_DIR",
|
|
37
|
+
"AGENT_BROWSER_SNAPSHOT_ID", "AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_STATE",
|
|
38
|
+
"AGENT_BROWSER_STATE_EXPIRE_DAYS", "AGENT_BROWSER_STREAM_PORT", "AGENT_BROWSER_USER_AGENT",
|
|
39
|
+
"AGENT_BROWSER_WEBGPU",
|
|
40
|
+
];
|
|
15
41
|
|
|
16
42
|
/**
|
|
17
43
|
* Render one public page in a fresh anonymous agent-browser session.
|
|
@@ -42,6 +68,13 @@ export async function renderWithAgentBrowser(
|
|
|
42
68
|
let tempDir = null;
|
|
43
69
|
let unregister = () => {};
|
|
44
70
|
let closed = false;
|
|
71
|
+
const renderDeadlineAt = Date.now() + BROWSER_TIMEOUT_MS;
|
|
72
|
+
|
|
73
|
+
function remainingRenderMs() {
|
|
74
|
+
const remaining = renderDeadlineAt - Date.now();
|
|
75
|
+
if (remaining <= 0) throw Object.assign(new Error(`agent-browser timed out after ${BROWSER_TIMEOUT_MS}ms`), { code: "browser_render_failed" });
|
|
76
|
+
return remaining;
|
|
77
|
+
}
|
|
45
78
|
|
|
46
79
|
async function closeSession() {
|
|
47
80
|
if (closed) return;
|
|
@@ -85,59 +118,9 @@ export async function renderWithAgentBrowser(
|
|
|
85
118
|
args: [...baseArgs, ...commandArgs],
|
|
86
119
|
cwd: workspace,
|
|
87
120
|
env: {
|
|
88
|
-
|
|
89
|
-
// override inherited from the host. Empty strings are not safe here:
|
|
90
|
-
// agent-browser treats some of them (notably SESSION_NAME) as
|
|
91
|
-
// configured-but-invalid values.
|
|
92
|
-
AGENT_BROWSER_ACTION_POLICY: undefined,
|
|
93
|
-
AGENT_BROWSER_ALLOWED_DOMAINS: undefined,
|
|
94
|
-
AGENT_BROWSER_ANNOTATE: undefined,
|
|
95
|
-
AGENT_BROWSER_ARGS: undefined,
|
|
96
|
-
AGENT_BROWSER_COLOR_SCHEME: undefined,
|
|
97
|
-
AGENT_BROWSER_CONFIRM_ACTIONS: undefined,
|
|
98
|
-
AGENT_BROWSER_CONFIRM_INTERACTIVE: undefined,
|
|
99
|
-
AGENT_BROWSER_CONTENT_BOUNDARIES: undefined,
|
|
100
|
-
AGENT_BROWSER_DEFAULT_TIMEOUT: undefined,
|
|
101
|
-
AGENT_BROWSER_DOWNLOAD_PATH: undefined,
|
|
102
|
-
AGENT_BROWSER_ENABLE: undefined,
|
|
103
|
-
AGENT_BROWSER_ENCRYPTION_KEY: undefined,
|
|
104
|
-
AGENT_BROWSER_ENGINE: undefined,
|
|
105
|
-
AGENT_BROWSER_EXECUTABLE_PATH: undefined,
|
|
106
|
-
AGENT_BROWSER_EXTENSIONS: undefined,
|
|
107
|
-
AGENT_BROWSER_HEADED: undefined,
|
|
108
|
-
AGENT_BROWSER_HIDE_SCROLLBARS: undefined,
|
|
109
|
-
AGENT_BROWSER_IDLE_TIMEOUT_MS: undefined,
|
|
110
|
-
AGENT_BROWSER_INIT_SCRIPTS: undefined,
|
|
111
|
-
AGENT_BROWSER_IOS_DEVICE: undefined,
|
|
112
|
-
AGENT_BROWSER_IOS_UDID: undefined,
|
|
113
|
-
AGENT_BROWSER_MAX_OUTPUT: undefined,
|
|
114
|
-
AGENT_BROWSER_NAMESPACE: undefined,
|
|
115
|
-
AGENT_BROWSER_NO_AUTO_DIALOG: undefined,
|
|
116
|
-
AGENT_BROWSER_NO_XVFB: undefined,
|
|
117
|
-
AGENT_BROWSER_PLUGINS: undefined,
|
|
118
|
-
AGENT_BROWSER_PROFILE: undefined,
|
|
119
|
-
AGENT_BROWSER_PROVIDER: undefined,
|
|
120
|
-
AGENT_BROWSER_PROXY: undefined,
|
|
121
|
-
AGENT_BROWSER_PROXY_BYPASS: undefined,
|
|
122
|
-
AGENT_BROWSER_RESTORE: undefined,
|
|
123
|
-
AGENT_BROWSER_RESTORE_CHECK_FN: undefined,
|
|
124
|
-
AGENT_BROWSER_RESTORE_CHECK_TEXT: undefined,
|
|
125
|
-
AGENT_BROWSER_RESTORE_CHECK_URL: undefined,
|
|
126
|
-
AGENT_BROWSER_SCREENSHOT_DIR: undefined,
|
|
127
|
-
AGENT_BROWSER_SCREENSHOT_FORMAT: undefined,
|
|
128
|
-
AGENT_BROWSER_SCREENSHOT_QUALITY: undefined,
|
|
129
|
-
AGENT_BROWSER_SESSION: undefined,
|
|
130
|
-
AGENT_BROWSER_SESSION_NAME: undefined,
|
|
131
|
-
AGENT_BROWSER_SKILLS_DIR: undefined,
|
|
132
|
-
AGENT_BROWSER_SOCKET_DIR: undefined,
|
|
133
|
-
AGENT_BROWSER_STATE: undefined,
|
|
134
|
-
AGENT_BROWSER_STATE_EXPIRE_DAYS: undefined,
|
|
135
|
-
AGENT_BROWSER_STREAM_PORT: undefined,
|
|
136
|
-
AGENT_BROWSER_USER_AGENT: undefined,
|
|
137
|
-
AGENT_BROWSER_WEBGPU: undefined,
|
|
121
|
+
...isolatedBrowserEnvironment(),
|
|
138
122
|
AGENT_BROWSER_AUTO_CONNECT: "false",
|
|
139
123
|
AGENT_BROWSER_AUTOSAVE_INTERVAL_MS: "0",
|
|
140
|
-
AGENT_BROWSER_CDP: undefined,
|
|
141
124
|
AGENT_BROWSER_CONFIG: configPath,
|
|
142
125
|
AGENT_BROWSER_RESTORE_SAVE: "never",
|
|
143
126
|
NO_COLOR: "1",
|
|
@@ -145,10 +128,14 @@ export async function renderWithAgentBrowser(
|
|
|
145
128
|
},
|
|
146
129
|
});
|
|
147
130
|
try {
|
|
148
|
-
const result = await runPreparedProcess(
|
|
131
|
+
const result = await runPreparedProcess({
|
|
132
|
+
...prepared,
|
|
133
|
+
env: isolatedBrowserEnvironment(prepared.env, configPath),
|
|
134
|
+
}, {
|
|
149
135
|
timeoutMs,
|
|
150
136
|
signal: abortSignal,
|
|
151
137
|
maxBufferBytes: BROWSER_OUTPUT_BYTES,
|
|
138
|
+
exactEnvironment: true,
|
|
152
139
|
});
|
|
153
140
|
if (result.timedOut) throw new Error(`agent-browser timed out after ${timeoutMs}ms`);
|
|
154
141
|
if (result.aborted) throw new Error("agent-browser was aborted");
|
|
@@ -156,7 +143,7 @@ export async function renderWithAgentBrowser(
|
|
|
156
143
|
if (result.spawnError) throw result.spawnError;
|
|
157
144
|
if (result.signal) throw new Error(`agent-browser terminated by ${result.signal}`);
|
|
158
145
|
if (result.code !== 0) {
|
|
159
|
-
throw new Error(`agent-browser exited ${result.code}
|
|
146
|
+
throw new Error(`agent-browser exited ${result.code}`);
|
|
160
147
|
}
|
|
161
148
|
return String(result.stdout || "").trim();
|
|
162
149
|
} finally {
|
|
@@ -168,17 +155,55 @@ export async function renderWithAgentBrowser(
|
|
|
168
155
|
tempDir = await mkdtemp(join(workspace, ".mono-agent-web-"));
|
|
169
156
|
await writeFile(join(tempDir, "agent-browser.json"), "{}\n", { encoding: "utf8", mode: 0o600 });
|
|
170
157
|
unregister = registerCleanup?.(closeSession) ?? (() => {});
|
|
171
|
-
await run(["open", parsed.href]);
|
|
172
|
-
await run(["wait", "--load", "domcontentloaded"]);
|
|
173
|
-
const
|
|
158
|
+
await run(["open", parsed.href], remainingRenderMs());
|
|
159
|
+
await run(["wait", "--load", "domcontentloaded"], remainingRenderMs());
|
|
160
|
+
const finalUrlOutput = await run(["get", "url"], remainingRenderMs());
|
|
161
|
+
const finalUrl = validateFinalUrl(extractBrowserText(finalUrlOutput), parsed, sandbox, policy);
|
|
162
|
+
const output = await run(["read"], remainingRenderMs());
|
|
174
163
|
const text = extractBrowserText(output);
|
|
175
164
|
if (!text) throw new Error("agent-browser returned no readable rendered content");
|
|
176
|
-
|
|
165
|
+
assertNoWebAccessInterstitial({ url: finalUrl, text });
|
|
166
|
+
return { text, finalUrl };
|
|
177
167
|
} finally {
|
|
178
168
|
await closeSession();
|
|
179
169
|
}
|
|
180
170
|
}
|
|
181
171
|
|
|
172
|
+
function validateFinalUrl(value, requested, sandbox, policy) {
|
|
173
|
+
let finalUrl;
|
|
174
|
+
try { finalUrl = new URL(String(value || "").trim()); }
|
|
175
|
+
catch { throw Object.assign(new Error("agent-browser returned an invalid final URL"), { code: "browser_render_failed" }); }
|
|
176
|
+
if (!["http:", "https:"].includes(finalUrl.protocol) || finalUrl.username || finalUrl.password) {
|
|
177
|
+
throw Object.assign(new Error("agent-browser navigated to an unsupported final URL"), { code: "network_denied" });
|
|
178
|
+
}
|
|
179
|
+
const requestedHost = requested.hostname.toLowerCase();
|
|
180
|
+
const finalHost = finalUrl.hostname.toLowerCase();
|
|
181
|
+
if (!(finalHost === requestedHost || finalHost.endsWith(`.${requestedHost}`))
|
|
182
|
+
|| !sandbox.networkAllowsUrl(policy, finalUrl.href)) {
|
|
183
|
+
throw Object.assign(new Error("agent-browser final URL is outside the allowed domain policy"), { code: "network_denied" });
|
|
184
|
+
}
|
|
185
|
+
return finalUrl.href;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isolatedBrowserEnvironment(source = process.env, configPath) {
|
|
189
|
+
/** @type {Record<string, string|undefined>} */
|
|
190
|
+
const env = {};
|
|
191
|
+
for (const key of BROWSER_HOST_ENV_KEYS) {
|
|
192
|
+
const value = source?.[key];
|
|
193
|
+
if (value !== undefined) env[key] = value;
|
|
194
|
+
}
|
|
195
|
+
// Undefined means deletion to the sandbox seam. runPreparedProcess then
|
|
196
|
+
// receives this map as an exact environment, so omitted host variables
|
|
197
|
+
// cannot reappear during its ordinary process.env merge.
|
|
198
|
+
for (const key of BLOCKED_BROWSER_ENV_KEYS) env[key] = undefined;
|
|
199
|
+
env.AGENT_BROWSER_AUTO_CONNECT = "false";
|
|
200
|
+
env.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS = "0";
|
|
201
|
+
if (configPath !== undefined) env.AGENT_BROWSER_CONFIG = configPath;
|
|
202
|
+
env.AGENT_BROWSER_RESTORE_SAVE = "never";
|
|
203
|
+
env.NO_COLOR = "1";
|
|
204
|
+
return env;
|
|
205
|
+
}
|
|
206
|
+
|
|
182
207
|
function compactBrowserNamespace(value) {
|
|
183
208
|
const candidate = String(value || "").trim();
|
|
184
209
|
if (/^[A-Za-z0-9_-]+$/u.test(candidate) && candidate.length <= MAX_BROWSER_NAMESPACE_CHARS) {
|
|
@@ -206,7 +231,7 @@ function findText(value, depth = 0) {
|
|
|
206
231
|
return value.map((entry) => findText(entry, depth + 1)).filter(Boolean).join("\n").trim();
|
|
207
232
|
}
|
|
208
233
|
if (typeof value !== "object") return "";
|
|
209
|
-
for (const key of ["markdown", "content", "text", "result", "output", "data"]) {
|
|
234
|
+
for (const key of ["markdown", "content", "text", "url", "result", "output", "data"]) {
|
|
210
235
|
if (!(key in value)) continue;
|
|
211
236
|
const text = findText(value[key], depth + 1);
|
|
212
237
|
if (text) return text;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
5
5
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
6
|
-
import { performWebFetch } from "./web-fetch.js";
|
|
6
|
+
import { performWebFetch, formatWebFetchDocument } from "./web-fetch.js";
|
|
7
7
|
import { performWebSearch } from "./web-search.js";
|
|
8
|
+
import { createWebSearchRunState, webSearchBudgetSnapshot } from "./web-search-state.js";
|
|
8
9
|
|
|
9
10
|
const MAX_CACHE_ENTRIES = 64;
|
|
10
11
|
const MAX_SHARED_SEARCH_ENTRIES = 256;
|
|
@@ -28,10 +29,12 @@ const sharedSearchCache = new Map();
|
|
|
28
29
|
* cleanup. Search results are the exception: they live in the process-wide
|
|
29
30
|
* cache above so sibling subagents and later turns can reuse them.
|
|
30
31
|
*
|
|
31
|
-
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
|
|
32
|
+
* @param {{coordinator?: any, searchConfig?: any, searchState?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
|
|
32
33
|
*/
|
|
33
34
|
export function createWebToolController({
|
|
35
|
+
coordinator,
|
|
34
36
|
searchConfig,
|
|
37
|
+
searchState: suppliedSearchState,
|
|
35
38
|
fetchConfig,
|
|
36
39
|
sandboxPolicy,
|
|
37
40
|
sandboxEngine,
|
|
@@ -46,6 +49,7 @@ export function createWebToolController({
|
|
|
46
49
|
const fetchInFlight = new Map();
|
|
47
50
|
const cleanups = new Set();
|
|
48
51
|
let closed = false;
|
|
52
|
+
const searchState = createWebSearchRunState(searchConfig, suppliedSearchState);
|
|
49
53
|
|
|
50
54
|
function registerCleanup(cleanup) {
|
|
51
55
|
if (closed) {
|
|
@@ -74,7 +78,7 @@ export function createWebToolController({
|
|
|
74
78
|
const result = await task;
|
|
75
79
|
if (!result.error) {
|
|
76
80
|
cache.set(key, cloneResult(result));
|
|
77
|
-
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
81
|
+
while (cache.size > MAX_CACHE_ENTRIES || [...cache.values()].reduce((n, r) => n + Buffer.byteLength(r.document?.body || "", "utf8"), 0) > 32 * 1024 * 1024) {
|
|
78
82
|
cache.delete(cache.keys().next().value);
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -88,12 +92,15 @@ export function createWebToolController({
|
|
|
88
92
|
* @param {string} key
|
|
89
93
|
* @param {() => Promise<any>} execute
|
|
90
94
|
*/
|
|
91
|
-
async function cachedSearch(key, execute) {
|
|
95
|
+
async function cachedSearch(key, query, execute) {
|
|
92
96
|
if (closed) return closedResult();
|
|
93
97
|
const cached = readSharedSearch(key);
|
|
94
|
-
if (cached) return
|
|
98
|
+
if (cached) return withSearchCacheHit(cached, searchState, query);
|
|
95
99
|
const active = searchInFlight.get(key);
|
|
96
|
-
if (active)
|
|
100
|
+
if (active) {
|
|
101
|
+
const result = await active;
|
|
102
|
+
return result.error ? withCacheHit(result) : withSearchCacheHit(result, searchState, query);
|
|
103
|
+
}
|
|
97
104
|
const task = Promise.resolve().then(execute);
|
|
98
105
|
searchInFlight.set(key, task);
|
|
99
106
|
try {
|
|
@@ -111,6 +118,7 @@ export function createWebToolController({
|
|
|
111
118
|
namespace,
|
|
112
119
|
|
|
113
120
|
async search(params, execution = {}) {
|
|
121
|
+
if (execution.signal?.aborted) return { text: "Error: WebSearch was aborted.", error: true, outcome: { status: "error", code: "aborted" } };
|
|
114
122
|
// The key must pin the backend, the endpoint AND the network policy the
|
|
115
123
|
// search actually ran under. A params-only key was safe while the cache
|
|
116
124
|
// lived and died with one run; process-wide it would let controllers with
|
|
@@ -132,30 +140,37 @@ export function createWebToolController({
|
|
|
132
140
|
// strict as the key claims.
|
|
133
141
|
const resolvedCtx = ctx ?? readToolRuntime();
|
|
134
142
|
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
135
|
-
const key = stableKey({ params, searchConfig, policy });
|
|
136
|
-
return cachedSearch(key, async () => performWebSearch(params, {
|
|
143
|
+
const key = stableKey({ params, searchConfig: safeSearchCacheIdentity(searchConfig), policy, coordination: coordinator?.scope });
|
|
144
|
+
return cachedSearch(key, params.query, async () => performWebSearch(params, {
|
|
145
|
+
coordinator,
|
|
137
146
|
searchConfig,
|
|
138
147
|
sandboxPolicy: policy,
|
|
139
148
|
ctx: resolvedCtx,
|
|
140
149
|
fetchImpl,
|
|
141
150
|
codexSearch,
|
|
151
|
+
searchState,
|
|
142
152
|
signal: execution.signal,
|
|
143
153
|
}));
|
|
144
154
|
},
|
|
145
155
|
|
|
146
156
|
async fetch(params, execution = {}) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
if (execution.signal?.aborted) return { text: "Error: WebFetch was aborted.", error: true, outcome: { status: "error", code: "aborted" } };
|
|
158
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
159
|
+
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
160
|
+
const { start_line, max_lines, max_output_chars, ...request } = params;
|
|
161
|
+
if ((start_line !== undefined && (!Number.isSafeInteger(start_line) || start_line < 1))
|
|
162
|
+
|| (max_lines !== undefined && (!Number.isSafeInteger(max_lines) || max_lines < 1 || max_lines > 10000))) {
|
|
163
|
+
return { text: "Error: Invalid WebFetch line range.", error: true, outcome: { status: "error", code: "invalid_range" } };
|
|
164
|
+
}
|
|
165
|
+
const key = stableKey({ request, fetchConfig, policy, coordination: coordinator?.scope });
|
|
166
|
+
const result = await cachedRun(fetchCache, fetchInFlight, key, async () => performWebFetch(request, {
|
|
167
|
+
documentOnly: true, coordinator, fetchConfig, sandboxPolicy: policy, sandboxEngine,
|
|
168
|
+
ctx: resolvedCtx, fetchImpl, browserRenderer, signal: execution.signal,
|
|
169
|
+
namespace, registerCleanup,
|
|
158
170
|
}));
|
|
171
|
+
if (result.error || !result.document) return result;
|
|
172
|
+
const sliced = formatWebFetchDocument({ ...result.document, outcome: result.outcome }, params, resolvedCtx);
|
|
173
|
+
return { ...sliced, document: undefined, outcome: { ...sliced.outcome, cacheHit: result.outcome.cacheHit } };
|
|
159
174
|
},
|
|
160
175
|
|
|
161
176
|
async close() {
|
|
@@ -174,6 +189,19 @@ export function createWebToolController({
|
|
|
174
189
|
};
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
function safeSearchCacheIdentity(searchConfig) {
|
|
193
|
+
if (!searchConfig || typeof searchConfig !== "object") return searchConfig;
|
|
194
|
+
const { maxRequestsPerRun: _budget, ...identity } = searchConfig;
|
|
195
|
+
if (typeof searchConfig?.ollama?.apiKey !== "string") return identity;
|
|
196
|
+
return {
|
|
197
|
+
...identity,
|
|
198
|
+
ollama: {
|
|
199
|
+
...searchConfig.ollama,
|
|
200
|
+
apiKey: `sha256:${createHash("sha256").update(searchConfig.ollama.apiKey).digest("hex")}`,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
function readSharedSearch(key) {
|
|
178
206
|
const entry = sharedSearchCache.get(key);
|
|
179
207
|
if (!entry) return null;
|
|
@@ -218,7 +246,15 @@ function sortValue(value) {
|
|
|
218
246
|
function cloneResult(result) {
|
|
219
247
|
return {
|
|
220
248
|
...result,
|
|
221
|
-
outcome: result.outcome ? {
|
|
249
|
+
outcome: result.outcome ? {
|
|
250
|
+
...result.outcome,
|
|
251
|
+
...(Array.isArray(result.outcome.providerAttempts)
|
|
252
|
+
? { providerAttempts: result.outcome.providerAttempts.map((entry) => ({ ...entry })) }
|
|
253
|
+
: {}),
|
|
254
|
+
...(Array.isArray(result.outcome.failureMetadata)
|
|
255
|
+
? { failureMetadata: result.outcome.failureMetadata.map((entry) => ({ ...entry })) }
|
|
256
|
+
: {}),
|
|
257
|
+
} : result.outcome,
|
|
222
258
|
};
|
|
223
259
|
}
|
|
224
260
|
|
|
@@ -228,10 +264,65 @@ function withCacheHit(result) {
|
|
|
228
264
|
outcome: {
|
|
229
265
|
...(result.outcome || {}),
|
|
230
266
|
cacheHit: true,
|
|
267
|
+
attempts: 0, durationMs: 0, queueWaitMs: 0, backendDurationMs: 0,
|
|
268
|
+
cooldownSkipCount: 0, quotaSkipCount: 0,
|
|
269
|
+
...(Number.isSafeInteger(result.outcome?.requestsThisCall) ? { requestsThisCall: 0 } : {}),
|
|
231
270
|
},
|
|
232
271
|
};
|
|
233
272
|
}
|
|
234
273
|
|
|
274
|
+
function withSearchCacheHit(result, searchState, requestedQuery) {
|
|
275
|
+
const cloned = withCacheHit(result);
|
|
276
|
+
const budget = webSearchBudgetSnapshot(searchState, 0);
|
|
277
|
+
const resultCount = Number.isSafeInteger(cloned.outcome?.resultCount) ? cloned.outcome.resultCount : 0;
|
|
278
|
+
const nextAction = resultCount > 0
|
|
279
|
+
? "fetch_existing_sources"
|
|
280
|
+
: budget.requestsRemaining > 0 ? "refine_query" : "use_available_evidence";
|
|
281
|
+
const action = nextAction === "fetch_existing_sources"
|
|
282
|
+
? "Use WebFetch on the strongest returned URLs before searching again."
|
|
283
|
+
: nextAction === "refine_query"
|
|
284
|
+
? "Refine the query only for a material evidence gap."
|
|
285
|
+
: "Do not retry WebSearch in this run; use available evidence and state the limitation.";
|
|
286
|
+
const control = `[Search control: requests=${budget.requestsUsed}/${budget.maxRequestsPerRun}; remaining=${budget.requestsRemaining}; ${action}]`;
|
|
287
|
+
const query = collapseWhitespace(requestedQuery).slice(0, 500);
|
|
288
|
+
const metadata = `[Search metadata: backend=${cloned.outcome?.backend || "unknown"}; attempted=none; actual_query=${JSON.stringify(query)}; fallback=none]`;
|
|
289
|
+
const textWithControl = typeof cloned.text === "string" && cloned.text.startsWith("[Search control:")
|
|
290
|
+
? cloned.text.replace(/^\[Search control:[^\n]*\]/u, control)
|
|
291
|
+
: `${control}\n${cloned.text}`;
|
|
292
|
+
const text = textWithControl.includes("[Search metadata:")
|
|
293
|
+
? textWithControl.replace(/^\[Search metadata:[^\n]*\]/mu, metadata)
|
|
294
|
+
: textWithControl.replace("[BEGIN UNTRUSTED WEB SEARCH RESULTS]", `[BEGIN UNTRUSTED WEB SEARCH RESULTS]\n${metadata}`);
|
|
295
|
+
const {
|
|
296
|
+
retryAfterMs: _retryAfterMs,
|
|
297
|
+
retryAt: _retryAt,
|
|
298
|
+
retryAtMs: _retryAtMs,
|
|
299
|
+
...cachedOutcome
|
|
300
|
+
} = cloned.outcome || {};
|
|
301
|
+
return {
|
|
302
|
+
...cloned,
|
|
303
|
+
text,
|
|
304
|
+
outcome: {
|
|
305
|
+
...cachedOutcome,
|
|
306
|
+
...budget,
|
|
307
|
+
bytes: Buffer.byteLength(text, "utf8"),
|
|
308
|
+
attemptedBackends: [],
|
|
309
|
+
actualQueries: [],
|
|
310
|
+
providerAttempts: [],
|
|
311
|
+
failureMetadata: [],
|
|
312
|
+
providerFailureCount: 0,
|
|
313
|
+
rateLimited: false,
|
|
314
|
+
cooldownBackends: [],
|
|
315
|
+
fallbackUsed: false,
|
|
316
|
+
retryInRun: budget.requestsRemaining > 0,
|
|
317
|
+
nextAction,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function collapseWhitespace(value) {
|
|
323
|
+
return typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
|
|
324
|
+
}
|
|
325
|
+
|
|
235
326
|
function closedResult() {
|
|
236
327
|
const text = "Error: Web tool controller has already closed.";
|
|
237
328
|
return {
|