@mono-agent/agent-runtime 0.20.2 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/agent/tools/codex-subscription-search.js +435 -0
- package/src/agent/tools/index.js +5 -0
- package/src/agent/tools/pi-bridge.js +1 -1
- package/src/agent/tools/web-controller.js +3 -1
- package/src/agent/tools/web-search.js +175 -41
- package/src/ai/types.js +1 -1
- package/types/agent/tools/codex-subscription-search.d.ts +31 -0
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/web-controller.d.ts +3 -2
- package/types/agent/tools/web-search.d.ts +13 -4
- package/types/ai/types.d.ts +5 -2
package/README.md
CHANGED
|
@@ -331,6 +331,7 @@ inferSkillsRoot
|
|
|
331
331
|
**`@mono-agent/agent-runtime/agent/tools/index.js`**
|
|
332
332
|
|
|
333
333
|
```text
|
|
334
|
+
DEFAULT_CODEX_SEARCH_MODEL
|
|
334
335
|
bashToolImpl
|
|
335
336
|
bashToolRun
|
|
336
337
|
createWebToolController
|
|
@@ -339,6 +340,7 @@ execToolImpl
|
|
|
339
340
|
execToolRun
|
|
340
341
|
globToolImpl
|
|
341
342
|
grepToolImpl
|
|
343
|
+
inspectCodexSubscriptionSearch
|
|
342
344
|
isPathAllowed
|
|
343
345
|
isWorkdirAllowed
|
|
344
346
|
normalizeBashTimeoutMs
|
|
@@ -347,6 +349,7 @@ performWebFetch
|
|
|
347
349
|
performWebSearch
|
|
348
350
|
readToolImpl
|
|
349
351
|
resolveRgPath
|
|
352
|
+
searchCodexSubscription
|
|
350
353
|
webFetchToolImpl
|
|
351
354
|
webSearchToolImpl
|
|
352
355
|
writeToolImpl
|
|
@@ -824,7 +827,7 @@ Per-call options (a non-exhaustive selection):
|
|
|
824
827
|
| `codexSandboxNetworkAccess` | `boolean` | Code-only Codex app-server per-turn network control. Only strict `true` enables it for plan/default/acceptEdits; omitted or any other value disables it. |
|
|
825
828
|
| `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http); on direct Codex, each forwarded server authorizes its own tool calls. |
|
|
826
829
|
| `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
|
|
827
|
-
| `webSearchConfig` | `{ backend?, endpoint? }` | Run-scoped local SearXNG
|
|
830
|
+
| `webSearchConfig` | `{ backend?, endpoint?, codex?: { model? } }` | Run-scoped local SearXNG, ChatGPT-subscription Codex, and keyless WebSearch backend selection. |
|
|
828
831
|
| `webFetchConfig` | `{ render?, browserCommand? }` | Run-scoped static extraction and optional isolated browser-render policy. |
|
|
829
832
|
| `piToolExecutionMode` | `"safe-parallel" \| "sequential"` | Pi built-in scheduling. Safe parallelism is the default; stateful/mutating and MCP tools stay sequential. |
|
|
830
833
|
| `maxTurns` | `number` | Hard cap on agent turns. |
|
package/package.json
CHANGED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { createCodexAppServerClient } from "../../ai/providers/codex-app.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_CODEX_SEARCH_MODEL = "gpt-5.6-luna";
|
|
10
|
+
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
12
|
+
const IDLE_CLOSE_MS = 1_000;
|
|
13
|
+
const MAX_MODEL_PAGES = 10;
|
|
14
|
+
const MAX_RESULTS = 100;
|
|
15
|
+
const SEARCH_ONLY_INSTRUCTIONS = [
|
|
16
|
+
"You are a search transport, not a general assistant.",
|
|
17
|
+
"Run exactly one live web search for the user's query, preserving quoted phrases and site: operators exactly.",
|
|
18
|
+
"Do not use shell, filesystem, MCP, apps, subagents, image, or any other tool.",
|
|
19
|
+
"Do not ask questions. The host consumes only the structured webSearch results and ignores your prose.",
|
|
20
|
+
].join(" ");
|
|
21
|
+
|
|
22
|
+
/** @type {{client: any, directory: string, models: Set<string>} | null} */
|
|
23
|
+
let broker = null;
|
|
24
|
+
/** @type {Promise<any> | null} */
|
|
25
|
+
let brokerOpening = null;
|
|
26
|
+
/** @type {Promise<void>} */
|
|
27
|
+
let serial = Promise.resolve();
|
|
28
|
+
/** @type {NodeJS.Timeout | null} */
|
|
29
|
+
let idleTimer = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inspect the installed Codex app-server without reading or exporting tokens.
|
|
33
|
+
*
|
|
34
|
+
* @param {{model?: string, clientFactory?: typeof createCodexAppServerClient}} [options]
|
|
35
|
+
*/
|
|
36
|
+
export async function inspectCodexSubscriptionSearch(options = {}) {
|
|
37
|
+
const model = normalizeModel(options.model);
|
|
38
|
+
let owned;
|
|
39
|
+
try {
|
|
40
|
+
owned = await openBroker(options.clientFactory);
|
|
41
|
+
const ready = await inspectClient(owned.client, model);
|
|
42
|
+
const { models: _models, ...publicReadiness } = ready;
|
|
43
|
+
return publicReadiness;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
code: "codex_unavailable",
|
|
48
|
+
reason: publicReason(error),
|
|
49
|
+
model,
|
|
50
|
+
};
|
|
51
|
+
} finally {
|
|
52
|
+
if (owned) await closeOwnedBroker(owned);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute one subscription-backed live search through Codex app-server.
|
|
58
|
+
* Calls are serialized process-wide so one agent cannot fan out subscription
|
|
59
|
+
* turns or cross-wire app-server notifications between requests.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} query
|
|
62
|
+
* @param {{model?: string, signal?: AbortSignal, clientFactory?: typeof createCodexAppServerClient}} [options]
|
|
63
|
+
*/
|
|
64
|
+
export function searchCodexSubscription(query, options = {}) {
|
|
65
|
+
return enqueue(async () => {
|
|
66
|
+
if (options.signal?.aborted) return abortedResult();
|
|
67
|
+
const model = normalizeModel(options.model);
|
|
68
|
+
let current;
|
|
69
|
+
try {
|
|
70
|
+
current = await getBroker(model, options.clientFactory);
|
|
71
|
+
const result = await runSearch(current, query, model, options.signal);
|
|
72
|
+
scheduleIdleClose();
|
|
73
|
+
return result;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await closeBroker();
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
backend: "codex",
|
|
79
|
+
message: `Codex subscription search unavailable: ${publicReason(error)}`,
|
|
80
|
+
retryable: isRetryable(error),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function enqueue(task) {
|
|
87
|
+
const result = serial.then(task, task);
|
|
88
|
+
serial = result.then(() => {}, () => {});
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function getBroker(model, clientFactory) {
|
|
93
|
+
clearIdleTimer();
|
|
94
|
+
if (broker?.models.has(model)) return broker;
|
|
95
|
+
if (broker && !broker.models.has(model)) await closeBroker();
|
|
96
|
+
if (!brokerOpening) {
|
|
97
|
+
brokerOpening = (async () => {
|
|
98
|
+
const owned = await openBroker(clientFactory);
|
|
99
|
+
const ready = await inspectClient(owned.client, model);
|
|
100
|
+
if (!ready.ok) {
|
|
101
|
+
await closeOwnedBroker(owned);
|
|
102
|
+
throw new Error(ready.reason);
|
|
103
|
+
}
|
|
104
|
+
owned.models = ready.models;
|
|
105
|
+
broker = owned;
|
|
106
|
+
return owned;
|
|
107
|
+
})().finally(() => { brokerOpening = null; });
|
|
108
|
+
}
|
|
109
|
+
return await brokerOpening;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function openBroker(clientFactory = createCodexAppServerClient) {
|
|
113
|
+
const directory = await mkdtemp(join(tmpdir(), "mono-agent-codex-search-"));
|
|
114
|
+
/** @type {{handler: (message: any) => void}} */
|
|
115
|
+
const target = { handler: () => {} };
|
|
116
|
+
let client;
|
|
117
|
+
try {
|
|
118
|
+
client = clientFactory({
|
|
119
|
+
cwd: directory,
|
|
120
|
+
onNotification: (message) => target.handler(message),
|
|
121
|
+
onServerRequest: (message) => {
|
|
122
|
+
target.handler(message);
|
|
123
|
+
throw new Error("Codex subscription search rejected an unexpected server request.");
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await client.request("initialize", {
|
|
127
|
+
clientInfo: { name: "mono-agent-web-search", title: "mono-agent WebSearch", version: "0" },
|
|
128
|
+
capabilities: { experimentalApi: true },
|
|
129
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
130
|
+
return { client, directory, models: new Set(), target };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
await Promise.resolve(client?.close?.()).catch(() => {});
|
|
133
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function inspectClient(client, model) {
|
|
139
|
+
const account = await client.request("account/read", { refreshToken: false }, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
140
|
+
if (account?.account?.type !== "chatgpt") {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
code: "codex_chatgpt_login_required",
|
|
144
|
+
reason: "Codex must be signed in with ChatGPT subscription access.",
|
|
145
|
+
model,
|
|
146
|
+
models: new Set(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const capabilities = await client.request(
|
|
150
|
+
"modelProvider/capabilities/read",
|
|
151
|
+
{},
|
|
152
|
+
{ timeoutMs: REQUEST_TIMEOUT_MS },
|
|
153
|
+
);
|
|
154
|
+
if (capabilities?.webSearch !== true) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
code: "codex_web_search_unavailable",
|
|
158
|
+
reason: "The signed-in Codex account does not expose web search.",
|
|
159
|
+
model,
|
|
160
|
+
models: new Set(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const models = await readModels(client);
|
|
164
|
+
if (!models.has(model)) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
code: "codex_model_unavailable",
|
|
168
|
+
reason: `Codex model ${model} is not available to the signed-in account.`,
|
|
169
|
+
model,
|
|
170
|
+
models,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
code: "ok",
|
|
176
|
+
reason: "",
|
|
177
|
+
model,
|
|
178
|
+
models,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readModels(client) {
|
|
183
|
+
const models = new Set();
|
|
184
|
+
let cursor = null;
|
|
185
|
+
for (let page = 0; page < MAX_MODEL_PAGES; page += 1) {
|
|
186
|
+
const response = await client.request("model/list", {
|
|
187
|
+
includeHidden: false,
|
|
188
|
+
limit: 100,
|
|
189
|
+
...(cursor === null ? {} : { cursor }),
|
|
190
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
191
|
+
if (!Array.isArray(response?.data)) throw new Error("Codex returned an invalid model catalog.");
|
|
192
|
+
for (const row of response.data) {
|
|
193
|
+
if (typeof row?.id === "string" && row.id.trim()) models.add(row.id.trim());
|
|
194
|
+
}
|
|
195
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor ? response.nextCursor : null;
|
|
196
|
+
if (cursor === null) return models;
|
|
197
|
+
}
|
|
198
|
+
throw new Error("Codex model catalog exceeded the pagination bound.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function runSearch(current, query, model, signal) {
|
|
202
|
+
const state = /** @type {any} */ ({
|
|
203
|
+
threadId: "",
|
|
204
|
+
turnId: "",
|
|
205
|
+
completed: false,
|
|
206
|
+
violation: "",
|
|
207
|
+
webSearchItems: [],
|
|
208
|
+
resolve: () => {},
|
|
209
|
+
});
|
|
210
|
+
const completion = new Promise((resolve) => { state.resolve = resolve; });
|
|
211
|
+
current.target.handler = (message) => handleNotification(message, state, current.client);
|
|
212
|
+
const onAbort = () => {
|
|
213
|
+
if (state.threadId && state.turnId) {
|
|
214
|
+
void current.client.request("turn/interrupt", {
|
|
215
|
+
threadId: state.threadId,
|
|
216
|
+
turnId: state.turnId,
|
|
217
|
+
}).catch(() => {});
|
|
218
|
+
}
|
|
219
|
+
state.violation ||= "WebSearch was aborted.";
|
|
220
|
+
state.resolve();
|
|
221
|
+
};
|
|
222
|
+
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
223
|
+
try {
|
|
224
|
+
const thread = await current.client.request("thread/start", {
|
|
225
|
+
model,
|
|
226
|
+
modelProvider: "openai",
|
|
227
|
+
allowProviderModelFallback: false,
|
|
228
|
+
cwd: current.directory,
|
|
229
|
+
runtimeWorkspaceRoots: [current.directory],
|
|
230
|
+
approvalPolicy: "untrusted",
|
|
231
|
+
sandbox: "read-only",
|
|
232
|
+
config: {
|
|
233
|
+
web_search: "live",
|
|
234
|
+
project_doc_max_bytes: 0,
|
|
235
|
+
mcp_servers: {},
|
|
236
|
+
},
|
|
237
|
+
developerInstructions: SEARCH_ONLY_INSTRUCTIONS,
|
|
238
|
+
ephemeral: true,
|
|
239
|
+
sessionStartSource: "startup",
|
|
240
|
+
environments: [],
|
|
241
|
+
dynamicTools: [],
|
|
242
|
+
selectedCapabilityRoots: [],
|
|
243
|
+
experimentalRawEvents: false,
|
|
244
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
245
|
+
state.threadId = thread?.thread?.id || "";
|
|
246
|
+
if (!state.threadId) throw new Error("Codex did not return a search thread id.");
|
|
247
|
+
const turn = await current.client.request("turn/start", {
|
|
248
|
+
threadId: state.threadId,
|
|
249
|
+
input: [{ type: "text", text: String(query), text_elements: [] }],
|
|
250
|
+
cwd: current.directory,
|
|
251
|
+
runtimeWorkspaceRoots: [current.directory],
|
|
252
|
+
approvalPolicy: "untrusted",
|
|
253
|
+
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
|
254
|
+
model,
|
|
255
|
+
effort: "low",
|
|
256
|
+
summary: "none",
|
|
257
|
+
environments: [],
|
|
258
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
259
|
+
state.turnId = turn?.turn?.id || state.turnId;
|
|
260
|
+
if (state.violation && state.turnId) {
|
|
261
|
+
await current.client.request("turn/interrupt", {
|
|
262
|
+
threadId: state.threadId,
|
|
263
|
+
turnId: state.turnId,
|
|
264
|
+
}).catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
await waitForCompletion(completion, signal);
|
|
267
|
+
if (state.violation) throw new Error(state.violation);
|
|
268
|
+
if (!state.completed) throw new Error("Codex search turn did not complete.");
|
|
269
|
+
if (state.webSearchItems.length !== 1) {
|
|
270
|
+
throw new Error(`Codex search turn produced ${state.webSearchItems.length} web search items; expected exactly one.`);
|
|
271
|
+
}
|
|
272
|
+
const item = state.webSearchItems[0];
|
|
273
|
+
const actualQuery = typeof item.query === "string" && item.query.trim()
|
|
274
|
+
? item.query.trim()
|
|
275
|
+
: String(query);
|
|
276
|
+
if (actualQuery !== String(query)) {
|
|
277
|
+
throw new Error("Codex changed the exact web search query.");
|
|
278
|
+
}
|
|
279
|
+
const results = normalizeResults(item.results);
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
backend: "codex",
|
|
283
|
+
results,
|
|
284
|
+
actualQuery,
|
|
285
|
+
};
|
|
286
|
+
} finally {
|
|
287
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
288
|
+
current.target.handler = () => {};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleNotification(message, state, client) {
|
|
293
|
+
const method = typeof message?.method === "string" ? message.method : "";
|
|
294
|
+
if (method === "turn/started" && message?.params?.threadId === state.threadId) {
|
|
295
|
+
state.turnId ||= message?.params?.turn?.id || "";
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if ((method === "item/started" || method === "item/completed")
|
|
299
|
+
&& message?.params?.threadId === state.threadId) {
|
|
300
|
+
const item = message?.params?.item;
|
|
301
|
+
if (item?.type === "webSearch") {
|
|
302
|
+
if (method === "item/completed") state.webSearchItems.push(item);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (["userMessage", "agentMessage", "reasoning", "plan"].includes(item?.type)) return;
|
|
306
|
+
state.violation ||= `Codex subscription search attempted unsupported item ${String(item?.type || "unknown")}.`;
|
|
307
|
+
if (state.threadId && state.turnId) {
|
|
308
|
+
void client.request("turn/interrupt", { threadId: state.threadId, turnId: state.turnId }).catch(() => {});
|
|
309
|
+
}
|
|
310
|
+
state.resolve();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (method === "turn/completed" && message?.params?.threadId === state.threadId) {
|
|
314
|
+
const turn = message?.params?.turn;
|
|
315
|
+
state.turnId ||= turn?.id || "";
|
|
316
|
+
state.completed = turn?.status === "completed";
|
|
317
|
+
if (!state.completed) state.violation ||= "Codex search turn failed.";
|
|
318
|
+
state.resolve();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (method === "error") {
|
|
322
|
+
state.violation ||= "Codex search turn reported an error.";
|
|
323
|
+
state.resolve();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (Object.prototype.hasOwnProperty.call(message || {}, "id") && method) {
|
|
327
|
+
state.violation ||= "Codex subscription search attempted an unsupported server request.";
|
|
328
|
+
state.resolve();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeResults(rows) {
|
|
333
|
+
if (!Array.isArray(rows)) return [];
|
|
334
|
+
const results = [];
|
|
335
|
+
for (const row of rows) {
|
|
336
|
+
if (!row || typeof row !== "object" || typeof row.url !== "string") continue;
|
|
337
|
+
results.push({
|
|
338
|
+
title: boundedText(row.title, 500),
|
|
339
|
+
url: row.url,
|
|
340
|
+
snippet: boundedText(row.snippet, 4_000),
|
|
341
|
+
provenance: boundedText(row.domain || row.ref_id || row.type, 300),
|
|
342
|
+
backend: "codex",
|
|
343
|
+
});
|
|
344
|
+
if (results.length >= MAX_RESULTS) break;
|
|
345
|
+
}
|
|
346
|
+
return results;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function waitForCompletion(completion, signal) {
|
|
350
|
+
let timer;
|
|
351
|
+
const timeout = new Promise((_, reject) => {
|
|
352
|
+
timer = setTimeout(() => reject(Object.assign(new Error("Codex search turn timed out."), {
|
|
353
|
+
code: "CODEX_SEARCH_TIMEOUT",
|
|
354
|
+
})), REQUEST_TIMEOUT_MS);
|
|
355
|
+
});
|
|
356
|
+
try {
|
|
357
|
+
if (signal?.aborted) throw Object.assign(new Error("WebSearch was aborted."), { name: "AbortError" });
|
|
358
|
+
await Promise.race([completion, timeout]);
|
|
359
|
+
} finally {
|
|
360
|
+
clearTimeout(timer);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function scheduleIdleClose() {
|
|
365
|
+
clearIdleTimer();
|
|
366
|
+
idleTimer = setTimeout(() => {
|
|
367
|
+
idleTimer = null;
|
|
368
|
+
void enqueue(async () => { await closeBroker(); });
|
|
369
|
+
}, IDLE_CLOSE_MS);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clearIdleTimer() {
|
|
373
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
374
|
+
idleTimer = null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function closeBroker() {
|
|
378
|
+
clearIdleTimer();
|
|
379
|
+
const owned = broker;
|
|
380
|
+
broker = null;
|
|
381
|
+
if (owned) await closeOwnedBroker(owned);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function closeOwnedBroker(owned) {
|
|
385
|
+
await Promise.resolve(owned.client?.close?.()).catch(() => {});
|
|
386
|
+
await rm(owned.directory, { recursive: true, force: true }).catch(() => {});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function normalizeModel(value) {
|
|
390
|
+
return typeof value === "string" && value.trim() ? value.trim() : DEFAULT_CODEX_SEARCH_MODEL;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function boundedText(value, max) {
|
|
394
|
+
return typeof value === "string" ? value.replace(/\s+/gu, " ").trim().slice(0, max) : "";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function safeReason(error) {
|
|
398
|
+
if (error?.name === "AbortError") return "WebSearch was aborted.";
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error || "unknown error");
|
|
400
|
+
return message.replace(/[\r\n\t]+/gu, " ").slice(0, 500) || "unknown error";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function publicReason(error) {
|
|
404
|
+
const reason = safeReason(error);
|
|
405
|
+
if (reason === "WebSearch was aborted.") return reason;
|
|
406
|
+
if (/timed out/iu.test(reason)) return "Codex app-server timed out.";
|
|
407
|
+
if (/signed in with ChatGPT subscription access/iu.test(reason)) return reason;
|
|
408
|
+
if (/does not expose web search/iu.test(reason)) return reason;
|
|
409
|
+
if (/Codex model .* is not available/iu.test(reason)) return reason;
|
|
410
|
+
if (/attempted unsupported item/iu.test(reason)) return "Codex attempted a non-search tool and the request was rejected.";
|
|
411
|
+
if (/unsupported server request/iu.test(reason)) return "Codex requested an unsupported interaction and the request was rejected.";
|
|
412
|
+
if (/produced .* web search items/iu.test(reason)) return "Codex did not produce exactly one structured web search result set.";
|
|
413
|
+
if (/changed the exact web search query/iu.test(reason)) return "Codex did not preserve the exact web search query.";
|
|
414
|
+
return "Codex app-server is not ready for subscription web search.";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isRetryable(error) {
|
|
418
|
+
return error?.name === "AbortError" || error?.code === "CODEX_SEARCH_TIMEOUT"
|
|
419
|
+
|| error?.code === "CODEX_APP_SERVER_REQUEST_TIMEOUT";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function abortedResult() {
|
|
423
|
+
return {
|
|
424
|
+
ok: false,
|
|
425
|
+
backend: "codex",
|
|
426
|
+
message: "WebSearch was aborted.",
|
|
427
|
+
retryable: false,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Test hook for process-shared broker state. */
|
|
432
|
+
export async function __resetCodexSubscriptionSearchForTests() {
|
|
433
|
+
await enqueue(async () => { await closeBroker(); });
|
|
434
|
+
brokerOpening = null;
|
|
435
|
+
}
|
package/src/agent/tools/index.js
CHANGED
|
@@ -18,6 +18,11 @@ export {
|
|
|
18
18
|
export { execToolImpl, execToolRun } from "./exec.js";
|
|
19
19
|
export { webFetchToolImpl, performWebFetch } from "./web-fetch.js";
|
|
20
20
|
export { webSearchToolImpl, performWebSearch } from "./web-search.js";
|
|
21
|
+
export {
|
|
22
|
+
DEFAULT_CODEX_SEARCH_MODEL,
|
|
23
|
+
inspectCodexSubscriptionSearch,
|
|
24
|
+
searchCodexSubscription,
|
|
25
|
+
} from "./codex-subscription-search.js";
|
|
21
26
|
export { createWebToolController } from "./web-controller.js";
|
|
22
27
|
|
|
23
28
|
export { isPathAllowed, isWorkdirAllowed } from "./shared/path-resolver.js";
|
|
@@ -575,7 +575,7 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
575
575
|
outcome: { status: "error", code: "controller_unavailable", retryable: false, attempts: 0 },
|
|
576
576
|
error: true,
|
|
577
577
|
}), toolContext),
|
|
578
|
-
WebSearch: createBuiltinTool("WebSearch", "Web Search", "Search the public web
|
|
578
|
+
WebSearch: createBuiltinTool("WebSearch", "Web Search", "Search the public web through local SearXNG, ChatGPT-subscription Codex search, and keyless fallbacks according to the configured backend, then return relevance-filtered deduplicated results.", objectSchema({
|
|
579
579
|
query: { type: "string" },
|
|
580
580
|
limit: { type: "integer" },
|
|
581
581
|
alternate_queries: { type: "array", items: { type: "string" }, maxItems: 3 },
|
|
@@ -28,7 +28,7 @@ const sharedSearchCache = new Map();
|
|
|
28
28
|
* cleanup. Search results are the exception: they live in the process-wide
|
|
29
29
|
* cache above so sibling subagents and later turns can reuse them.
|
|
30
30
|
*
|
|
31
|
-
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any}} [options]
|
|
31
|
+
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
|
|
32
32
|
*/
|
|
33
33
|
export function createWebToolController({
|
|
34
34
|
searchConfig,
|
|
@@ -38,6 +38,7 @@ export function createWebToolController({
|
|
|
38
38
|
ctx,
|
|
39
39
|
fetchImpl,
|
|
40
40
|
browserRenderer,
|
|
41
|
+
codexSearch,
|
|
41
42
|
} = {}) {
|
|
42
43
|
const namespace = `mono-agent-web-${randomUUID()}`;
|
|
43
44
|
const fetchCache = new Map();
|
|
@@ -137,6 +138,7 @@ export function createWebToolController({
|
|
|
137
138
|
sandboxPolicy: policy,
|
|
138
139
|
ctx: resolvedCtx,
|
|
139
140
|
fetchImpl,
|
|
141
|
+
codexSearch,
|
|
140
142
|
signal: execution.signal,
|
|
141
143
|
}));
|
|
142
144
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { parseHTML } from "linkedom";
|
|
4
4
|
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
5
|
+
import { searchCodexSubscription } from "./codex-subscription-search.js";
|
|
5
6
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
6
7
|
import { createCountingSemaphore } from "./shared/semaphore.js";
|
|
7
8
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
@@ -81,11 +82,12 @@ export async function webSearchToolImpl(params, options = {}) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* Search through an operator-owned SearXNG endpoint
|
|
85
|
-
* fallback chain. Returns a structured
|
|
85
|
+
* Search through an operator-owned SearXNG endpoint, ChatGPT-subscription
|
|
86
|
+
* Codex search, and/or the keyless HTML fallback chain. Returns a structured
|
|
87
|
+
* internal outcome for the Pi bridge.
|
|
86
88
|
*
|
|
87
89
|
* @param {{query: string, limit?: number, alternate_queries?: string[], domains?: string[], exclude_domains?: string[], language?: string, time_range?: string}} params
|
|
88
|
-
* @param {{sandboxPolicy?: any, ctx?: any, signal?: AbortSignal, searchConfig?: any, fetchImpl?: typeof fetch}} [options]
|
|
90
|
+
* @param {{sandboxPolicy?: any, ctx?: any, signal?: AbortSignal, searchConfig?: any, fetchImpl?: typeof fetch, codexSearch?: typeof searchCodexSubscription}} [options]
|
|
89
91
|
*/
|
|
90
92
|
export async function performWebSearch(
|
|
91
93
|
{
|
|
@@ -103,6 +105,7 @@ export async function performWebSearch(
|
|
|
103
105
|
signal,
|
|
104
106
|
searchConfig,
|
|
105
107
|
fetchImpl = globalThis.fetch,
|
|
108
|
+
codexSearch = searchCodexSubscription,
|
|
106
109
|
} = {},
|
|
107
110
|
) {
|
|
108
111
|
const startedAt = Date.now();
|
|
@@ -111,7 +114,8 @@ export async function performWebSearch(
|
|
|
111
114
|
return failure("Error: WebSearch query must not be empty.", "invalid_query", startedAt);
|
|
112
115
|
}
|
|
113
116
|
const max = clampInteger(limit, 1, 10, 5);
|
|
114
|
-
const
|
|
117
|
+
const explicitDomains = normalizeDomains(Array.isArray(domains) ? domains : []);
|
|
118
|
+
const includeDomains = normalizeDomains([...explicitDomains, ...querySiteDomains(normalizedQuery)]);
|
|
115
119
|
const excludeDomains = normalizeDomains(exclude_domains);
|
|
116
120
|
const config = normalizeSearchConfig(searchConfig);
|
|
117
121
|
if (config.error) return failure(`Error: ${config.error}`, "invalid_search_config", startedAt);
|
|
@@ -119,26 +123,33 @@ export async function performWebSearch(
|
|
|
119
123
|
const resolvedCtx = ctx ?? readToolRuntime();
|
|
120
124
|
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
121
125
|
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
126
|
+
// Operators, quotes, and site: constraints are never relaxed or rewritten by
|
|
127
|
+
// the host. Alternate queries are explicit model input, not host-generated
|
|
128
|
+
// substitutions for the user's exact primary query.
|
|
122
129
|
const initialQueries = uniqueStrings([normalizedQuery, ...alternate_queries], 4);
|
|
123
130
|
/** @type {Array<Array<{title: string, url: string, snippet: string, backend: string}>>} */
|
|
124
131
|
const rankedLists = [];
|
|
125
132
|
const providerFailures = [];
|
|
126
133
|
const providersUsed = new Set();
|
|
134
|
+
const attemptedBackends = new Set();
|
|
135
|
+
const actualQueries = [];
|
|
127
136
|
let attempts = 0;
|
|
128
137
|
let anyProviderSucceeded = false;
|
|
129
138
|
|
|
130
|
-
const runQuery = async (candidate) => {
|
|
139
|
+
const runQuery = async (candidate, backend) => {
|
|
131
140
|
attempts += 1;
|
|
141
|
+
attemptedBackends.add(backend);
|
|
132
142
|
return await searchOneQuery(
|
|
133
|
-
queryWithDomains(candidate,
|
|
143
|
+
queryWithDomains(candidate, explicitDomains),
|
|
134
144
|
{
|
|
135
|
-
config,
|
|
145
|
+
config: { ...config, backend },
|
|
136
146
|
language,
|
|
137
147
|
timeRange: time_range,
|
|
138
148
|
sandbox,
|
|
139
149
|
policy,
|
|
140
150
|
signal,
|
|
141
151
|
fetchImpl,
|
|
152
|
+
codexSearch,
|
|
142
153
|
},
|
|
143
154
|
);
|
|
144
155
|
};
|
|
@@ -147,16 +158,51 @@ export async function performWebSearch(
|
|
|
147
158
|
// so a silent degradation to the fallback is still visible in the outcome.
|
|
148
159
|
if (result.failures?.length) providerFailures.push(...result.failures);
|
|
149
160
|
if (result.ok) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
const filtered = filterRelevantResults(
|
|
162
|
+
filterByDomains(result.results, includeDomains, excludeDomains),
|
|
163
|
+
normalizedQuery,
|
|
164
|
+
);
|
|
165
|
+
if (filtered.length > 0) {
|
|
166
|
+
anyProviderSucceeded = true;
|
|
167
|
+
providersUsed.add(result.backend);
|
|
168
|
+
rankedLists.push(filtered);
|
|
169
|
+
} else if (result.results.length === 0) {
|
|
170
|
+
anyProviderSucceeded = true;
|
|
171
|
+
} else {
|
|
172
|
+
providerFailures.push({
|
|
173
|
+
ok: false,
|
|
174
|
+
backend: result.backend,
|
|
175
|
+
message: `${result.backend} returned no relevant results.`,
|
|
176
|
+
retryable: false,
|
|
177
|
+
relevance: true,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (typeof result.actualQuery === "string" && result.actualQuery.trim()) {
|
|
181
|
+
actualQueries.push(result.actualQuery.trim());
|
|
182
|
+
}
|
|
153
183
|
} else if (!result.failures?.length) {
|
|
154
184
|
providerFailures.push(result);
|
|
155
185
|
}
|
|
156
186
|
};
|
|
157
187
|
|
|
158
|
-
const
|
|
159
|
-
|
|
188
|
+
const runStage = async (backend, candidates) => {
|
|
189
|
+
const results = await Promise.all(candidates.map((candidate) => runQuery(candidate, backend)));
|
|
190
|
+
results.forEach(recordResult);
|
|
191
|
+
return mergeRankedResults(rankedLists, max);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let merged = [];
|
|
195
|
+
if (config.backend === "searxng" || (config.backend === "auto" && config.endpoint)) {
|
|
196
|
+
merged = await runStage("searxng", initialQueries);
|
|
197
|
+
}
|
|
198
|
+
if (config.backend === "codex" || (config.backend === "auto" && merged.length === 0)) {
|
|
199
|
+
// Exactly one subscription turn per WebSearch call. Alternate queries still
|
|
200
|
+
// help local/keyless rank fusion but never multiply paid subscription work.
|
|
201
|
+
merged = await runStage("codex", [normalizedQuery]);
|
|
202
|
+
}
|
|
203
|
+
if (config.backend === "keyless" || (config.backend === "auto" && merged.length === 0)) {
|
|
204
|
+
merged = await runStage("keyless", initialQueries);
|
|
205
|
+
}
|
|
160
206
|
if (signal?.aborted) {
|
|
161
207
|
return failure("Error: WebSearch was aborted.", "aborted", startedAt, {
|
|
162
208
|
attempts,
|
|
@@ -164,15 +210,6 @@ export async function performWebSearch(
|
|
|
164
210
|
});
|
|
165
211
|
}
|
|
166
212
|
|
|
167
|
-
let merged = mergeRankedResults(rankedLists, max);
|
|
168
|
-
if (merged.length < max && initialQueries.length < 4) {
|
|
169
|
-
const relaxed = relaxedQuery(normalizedQuery);
|
|
170
|
-
if (relaxed && !initialQueries.includes(relaxed)) {
|
|
171
|
-
recordResult(await runQuery(relaxed));
|
|
172
|
-
merged = mergeRankedResults(rankedLists, max);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
213
|
if (!anyProviderSucceeded) {
|
|
177
214
|
// Four query variants against two backends produce the same handful of
|
|
178
215
|
// messages over and over; dedupe so the reason stays readable.
|
|
@@ -190,10 +227,14 @@ export async function performWebSearch(
|
|
|
190
227
|
retryable: providerFailures.some((entry) => entry.retryable),
|
|
191
228
|
rateLimited: throttled,
|
|
192
229
|
cooldownBackends: [...backendCooldownUntil.keys()],
|
|
230
|
+
attemptedBackends: [...attemptedBackends],
|
|
231
|
+
failureMetadata: sanitizeFailureMetadata(providerFailures),
|
|
193
232
|
});
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
const backend = providersUsed.size === 1
|
|
235
|
+
const backend = providersUsed.size === 1
|
|
236
|
+
? [...providersUsed][0]
|
|
237
|
+
: providersUsed.size > 1 ? "mixed" : config.backend;
|
|
197
238
|
const body = merged.length === 0
|
|
198
239
|
? "No results."
|
|
199
240
|
: merged.map((result, index) => {
|
|
@@ -202,6 +243,12 @@ export async function performWebSearch(
|
|
|
202
243
|
}).join("\n\n");
|
|
203
244
|
const text = [
|
|
204
245
|
"[BEGIN UNTRUSTED WEB SEARCH RESULTS]",
|
|
246
|
+
searchMetadataLine({
|
|
247
|
+
backend,
|
|
248
|
+
attemptedBackends: [...attemptedBackends],
|
|
249
|
+
query: actualQueries[0] || normalizedQuery,
|
|
250
|
+
providerFailures,
|
|
251
|
+
}),
|
|
205
252
|
body,
|
|
206
253
|
"[END UNTRUSTED WEB SEARCH RESULTS]",
|
|
207
254
|
].join("\n");
|
|
@@ -221,6 +268,9 @@ export async function performWebSearch(
|
|
|
221
268
|
providerFailureCount: providerFailures.length,
|
|
222
269
|
rateLimited: providerFailures.some((entry) => entry.rateLimited || entry.cooldown),
|
|
223
270
|
cooldownBackends: [...backendCooldownUntil.keys()],
|
|
271
|
+
attemptedBackends: [...attemptedBackends],
|
|
272
|
+
actualQueries: uniqueStrings(actualQueries.length > 0 ? actualQueries : [normalizedQuery], 4),
|
|
273
|
+
failureMetadata: sanitizeFailureMetadata(providerFailures),
|
|
224
274
|
},
|
|
225
275
|
error: false,
|
|
226
276
|
};
|
|
@@ -243,16 +293,27 @@ async function searchOneQuery(query, options) {
|
|
|
243
293
|
// circuit `auto` outright, so the fallbacks below could never rescue a query.
|
|
244
294
|
let emptySuccess = null;
|
|
245
295
|
if (options.signal?.aborted) return abortedSearch(config.backend, failures);
|
|
246
|
-
if (config.backend === "searxng"
|
|
296
|
+
if (config.backend === "searxng") {
|
|
247
297
|
const result = await searchSearxng(query, options);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
298
|
+
return { ...result, failures };
|
|
299
|
+
}
|
|
300
|
+
if (config.backend === "codex") {
|
|
301
|
+
if (!options.sandbox.networkAllowsUrl(options.policy, "https://chatgpt.com")) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
backend: "codex",
|
|
305
|
+
message: "Network access denied by sandbox policy.",
|
|
306
|
+
retryable: false,
|
|
307
|
+
failures,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const result = await options.codexSearch(query, {
|
|
311
|
+
model: config.codex.model,
|
|
312
|
+
signal: options.signal,
|
|
313
|
+
});
|
|
314
|
+
return { ...result, failures };
|
|
315
|
+
}
|
|
316
|
+
if (config.backend === "keyless") {
|
|
256
317
|
for (const backend of KEYLESS_BACKENDS) {
|
|
257
318
|
if (options.signal?.aborted) return abortedSearch(backend, failures);
|
|
258
319
|
if (backendInCooldown(backend)) {
|
|
@@ -694,8 +755,8 @@ export function mergeRankedResults(rankedLists, limit = 10) {
|
|
|
694
755
|
|
|
695
756
|
function normalizeSearchConfig(input) {
|
|
696
757
|
const backend = input?.backend ?? "auto";
|
|
697
|
-
if (!["auto", "searxng", "keyless"].includes(backend)) {
|
|
698
|
-
return { error: "Web search backend must be auto, searxng, or keyless." };
|
|
758
|
+
if (!["auto", "searxng", "codex", "keyless"].includes(backend)) {
|
|
759
|
+
return { error: "Web search backend must be auto, searxng, codex, or keyless." };
|
|
699
760
|
}
|
|
700
761
|
let endpoint;
|
|
701
762
|
if (input?.endpoint !== undefined && String(input.endpoint).trim()) {
|
|
@@ -716,7 +777,13 @@ function normalizeSearchConfig(input) {
|
|
|
716
777
|
if (backend === "searxng" && !endpoint) {
|
|
717
778
|
return { error: "SearXNG backend requires tools.web.search.endpoint." };
|
|
718
779
|
}
|
|
719
|
-
|
|
780
|
+
const model = typeof input?.codex?.model === "string" && input.codex.model.trim()
|
|
781
|
+
? input.codex.model.trim()
|
|
782
|
+
: "gpt-5.6-luna";
|
|
783
|
+
if (model.length > 160 || /[\u0000-\u001f\u007f]/u.test(model)) {
|
|
784
|
+
return { error: "Codex web search model must be a valid model id." };
|
|
785
|
+
}
|
|
786
|
+
return { backend, endpoint, codex: { model } };
|
|
720
787
|
}
|
|
721
788
|
|
|
722
789
|
function isLoopbackHost(hostname) {
|
|
@@ -746,6 +813,48 @@ function filterByDomains(results, include, exclude) {
|
|
|
746
813
|
});
|
|
747
814
|
}
|
|
748
815
|
|
|
816
|
+
const RELEVANCE_STOP_WORDS = new Set([
|
|
817
|
+
"about", "after", "before", "best", "find", "from", "into", "latest",
|
|
818
|
+
"near", "news", "that", "the", "their", "this", "time", "what", "when",
|
|
819
|
+
"where", "which", "with", "your",
|
|
820
|
+
]);
|
|
821
|
+
|
|
822
|
+
function filterRelevantResults(results, query) {
|
|
823
|
+
const phrases = [...String(query).matchAll(/"([^"]{2,})"/gu)]
|
|
824
|
+
.map((match) => comparableText(match[1]))
|
|
825
|
+
.filter(Boolean);
|
|
826
|
+
const terms = uniqueStrings(
|
|
827
|
+
comparableText(String(query)
|
|
828
|
+
.replace(/"[^"]*"/gu, " ")
|
|
829
|
+
.replace(/\bsite:\S+/giu, " "))
|
|
830
|
+
.split(" ")
|
|
831
|
+
.filter((term) => term.length >= 3 && !RELEVANCE_STOP_WORDS.has(term)),
|
|
832
|
+
20,
|
|
833
|
+
);
|
|
834
|
+
if (phrases.length === 0 && terms.length === 0) return results;
|
|
835
|
+
const requiredTerms = Math.min(terms.length, terms.length >= 3 ? 2 : 1);
|
|
836
|
+
return results.filter((result) => {
|
|
837
|
+
const haystack = comparableText(`${result.title} ${result.snippet} ${result.url}`);
|
|
838
|
+
if (phrases.some((phrase) => !haystack.includes(phrase))) return false;
|
|
839
|
+
if (requiredTerms === 0) return true;
|
|
840
|
+
let matches = 0;
|
|
841
|
+
for (const term of terms) {
|
|
842
|
+
if (haystack.includes(term)) matches += 1;
|
|
843
|
+
if (matches >= requiredTerms) return true;
|
|
844
|
+
}
|
|
845
|
+
return false;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function comparableText(value) {
|
|
850
|
+
return String(value || "")
|
|
851
|
+
.normalize("NFKC")
|
|
852
|
+
.toLowerCase()
|
|
853
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
854
|
+
.replace(/\s+/gu, " ")
|
|
855
|
+
.trim();
|
|
856
|
+
}
|
|
857
|
+
|
|
749
858
|
function domainMatches(host, domain) {
|
|
750
859
|
return host === domain || host.endsWith(`.${domain}`);
|
|
751
860
|
}
|
|
@@ -755,13 +864,38 @@ function queryWithDomains(query, domains) {
|
|
|
755
864
|
return `${query} (${domains.map((domain) => `site:${domain}`).join(" OR ")})`;
|
|
756
865
|
}
|
|
757
866
|
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
867
|
+
function querySiteDomains(query) {
|
|
868
|
+
return [...String(query).matchAll(/\bsite:([a-z0-9.-]+)(?:\/\S*)?/giu)]
|
|
869
|
+
.map((match) => match[1]);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function sanitizeFailureMetadata(failures) {
|
|
873
|
+
const seen = new Set();
|
|
874
|
+
const metadata = [];
|
|
875
|
+
for (const failureEntry of failures) {
|
|
876
|
+
const backend = collapseWhitespace(failureEntry?.backend).slice(0, 40) || "unknown";
|
|
877
|
+
const code = failureEntry?.relevance
|
|
878
|
+
? "no_relevant_results"
|
|
879
|
+
: failureEntry?.rateLimited ? "rate_limited"
|
|
880
|
+
: failureEntry?.cooldown ? "cooldown"
|
|
881
|
+
: failureEntry?.message === "Network access denied by sandbox policy."
|
|
882
|
+
? "network_denied"
|
|
883
|
+
: "unavailable";
|
|
884
|
+
const key = `${backend}:${code}`;
|
|
885
|
+
if (seen.has(key)) continue;
|
|
886
|
+
seen.add(key);
|
|
887
|
+
metadata.push({ backend, code });
|
|
888
|
+
if (metadata.length >= 12) break;
|
|
889
|
+
}
|
|
890
|
+
return metadata;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function searchMetadataLine({ backend, attemptedBackends, query, providerFailures }) {
|
|
894
|
+
const attempted = attemptedBackends.join(",") || "none";
|
|
895
|
+
const failures = sanitizeFailureMetadata(providerFailures)
|
|
896
|
+
.map((entry) => `${entry.backend}:${entry.code}`)
|
|
897
|
+
.join(",") || "none";
|
|
898
|
+
return `[Search metadata: backend=${backend}; attempted=${attempted}; actual_query=${JSON.stringify(collapseWhitespace(query).slice(0, 500))}; fallback=${failures}]`;
|
|
765
899
|
}
|
|
766
900
|
|
|
767
901
|
function uniqueStrings(values, limit) {
|
package/src/ai/types.js
CHANGED
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
* @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Per-run ACP profile resolver; wins over the host default.
|
|
288
288
|
* @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Per-run ACP permission/elicitation callback; wins over the host default.
|
|
289
289
|
* @property {Uint8Array} [acpSessionTokenKey] Host-owned 32-byte key for confidential authenticated ACP session handles. Required for every ACP task run.
|
|
290
|
-
* @property {{backend?: "auto"|"searxng"|"keyless", endpoint?: string}} [webSearchConfig] Run-scoped WebSearch backend configuration.
|
|
290
|
+
* @property {{backend?: "auto"|"searxng"|"codex"|"keyless", endpoint?: string, codex?: {model?: string}}} [webSearchConfig] Run-scoped WebSearch backend configuration.
|
|
291
291
|
* @property {{render?: "never"|"auto", browserCommand?: string}} [webFetchConfig] Run-scoped WebFetch extraction/render configuration.
|
|
292
292
|
* @property {"sequential"|"safe-parallel"} [piToolExecutionMode] Pi built-in tool scheduling mode. Safe parallelism is the default.
|
|
293
293
|
* @property {"one-at-a-time"|"all"} [piToolParallelismMode] DEPRECATED. Compatibility alias mapped to piToolExecutionMode.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspect the installed Codex app-server without reading or exporting tokens.
|
|
3
|
+
*
|
|
4
|
+
* @param {{model?: string, clientFactory?: typeof createCodexAppServerClient}} [options]
|
|
5
|
+
*/
|
|
6
|
+
export function inspectCodexSubscriptionSearch(options?: {
|
|
7
|
+
model?: string;
|
|
8
|
+
clientFactory?: typeof createCodexAppServerClient;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
code: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
model: any;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Execute one subscription-backed live search through Codex app-server.
|
|
17
|
+
* Calls are serialized process-wide so one agent cannot fan out subscription
|
|
18
|
+
* turns or cross-wire app-server notifications between requests.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} query
|
|
21
|
+
* @param {{model?: string, signal?: AbortSignal, clientFactory?: typeof createCodexAppServerClient}} [options]
|
|
22
|
+
*/
|
|
23
|
+
export function searchCodexSubscription(query: string, options?: {
|
|
24
|
+
model?: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
clientFactory?: typeof createCodexAppServerClient;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
/** Test hook for process-shared broker state. */
|
|
29
|
+
export function __resetCodexSubscriptionSearchForTests(): Promise<void>;
|
|
30
|
+
export const DEFAULT_CODEX_SEARCH_MODEL: "gpt-5.6-luna";
|
|
31
|
+
import { createCodexAppServerClient } from "../../ai/providers/codex-app.js";
|
|
@@ -9,4 +9,5 @@ export { bashToolImpl, bashToolRun, normalizeBashTimeoutMs, normalizeProcessTime
|
|
|
9
9
|
export { execToolImpl, execToolRun } from "./exec.js";
|
|
10
10
|
export { webFetchToolImpl, performWebFetch } from "./web-fetch.js";
|
|
11
11
|
export { webSearchToolImpl, performWebSearch } from "./web-search.js";
|
|
12
|
+
export { DEFAULT_CODEX_SEARCH_MODEL, inspectCodexSubscriptionSearch, searchCodexSubscription } from "./codex-subscription-search.js";
|
|
12
13
|
export { isPathAllowed, isWorkdirAllowed } from "./shared/path-resolver.js";
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* cleanup. Search results are the exception: they live in the process-wide
|
|
5
5
|
* cache above so sibling subagents and later turns can reuse them.
|
|
6
6
|
*
|
|
7
|
-
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any}} [options]
|
|
7
|
+
* @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any, codexSearch?: any}} [options]
|
|
8
8
|
*/
|
|
9
|
-
export function createWebToolController({ searchConfig, fetchConfig, sandboxPolicy, sandboxEngine, ctx, fetchImpl, browserRenderer, }?: {
|
|
9
|
+
export function createWebToolController({ searchConfig, fetchConfig, sandboxPolicy, sandboxEngine, ctx, fetchImpl, browserRenderer, codexSearch, }?: {
|
|
10
10
|
searchConfig?: any;
|
|
11
11
|
fetchConfig?: any;
|
|
12
12
|
sandboxPolicy?: any;
|
|
@@ -14,6 +14,7 @@ export function createWebToolController({ searchConfig, fetchConfig, sandboxPoli
|
|
|
14
14
|
ctx?: any;
|
|
15
15
|
fetchImpl?: typeof fetch;
|
|
16
16
|
browserRenderer?: any;
|
|
17
|
+
codexSearch?: any;
|
|
17
18
|
}): {
|
|
18
19
|
namespace: string;
|
|
19
20
|
search(params: any, execution?: {}): Promise<any>;
|
|
@@ -20,11 +20,12 @@ export function webSearchToolImpl(params: {
|
|
|
20
20
|
fetchImpl?: typeof fetch;
|
|
21
21
|
}): Promise<any>;
|
|
22
22
|
/**
|
|
23
|
-
* Search through an operator-owned SearXNG endpoint
|
|
24
|
-
* fallback chain. Returns a structured
|
|
23
|
+
* Search through an operator-owned SearXNG endpoint, ChatGPT-subscription
|
|
24
|
+
* Codex search, and/or the keyless HTML fallback chain. Returns a structured
|
|
25
|
+
* internal outcome for the Pi bridge.
|
|
25
26
|
*
|
|
26
27
|
* @param {{query: string, limit?: number, alternate_queries?: string[], domains?: string[], exclude_domains?: string[], language?: string, time_range?: string}} params
|
|
27
|
-
* @param {{sandboxPolicy?: any, ctx?: any, signal?: AbortSignal, searchConfig?: any, fetchImpl?: typeof fetch}} [options]
|
|
28
|
+
* @param {{sandboxPolicy?: any, ctx?: any, signal?: AbortSignal, searchConfig?: any, fetchImpl?: typeof fetch, codexSearch?: typeof searchCodexSubscription}} [options]
|
|
28
29
|
*/
|
|
29
30
|
export function performWebSearch({ query, limit, alternate_queries, domains, exclude_domains, language, time_range, }: {
|
|
30
31
|
query: string;
|
|
@@ -34,12 +35,13 @@ export function performWebSearch({ query, limit, alternate_queries, domains, exc
|
|
|
34
35
|
exclude_domains?: string[];
|
|
35
36
|
language?: string;
|
|
36
37
|
time_range?: string;
|
|
37
|
-
}, { sandboxPolicy, ctx, signal, searchConfig, fetchImpl, }?: {
|
|
38
|
+
}, { sandboxPolicy, ctx, signal, searchConfig, fetchImpl, codexSearch, }?: {
|
|
38
39
|
sandboxPolicy?: any;
|
|
39
40
|
ctx?: any;
|
|
40
41
|
signal?: AbortSignal;
|
|
41
42
|
searchConfig?: any;
|
|
42
43
|
fetchImpl?: typeof fetch;
|
|
44
|
+
codexSearch?: typeof searchCodexSubscription;
|
|
43
45
|
}): Promise<{
|
|
44
46
|
text: any;
|
|
45
47
|
outcome: {
|
|
@@ -70,6 +72,12 @@ export function performWebSearch({ query, limit, alternate_queries, domains, exc
|
|
|
70
72
|
providerFailureCount: number;
|
|
71
73
|
rateLimited: boolean;
|
|
72
74
|
cooldownBackends: string[];
|
|
75
|
+
attemptedBackends: any[];
|
|
76
|
+
actualQueries: string[];
|
|
77
|
+
failureMetadata: {
|
|
78
|
+
backend: string;
|
|
79
|
+
code: string;
|
|
80
|
+
}[];
|
|
73
81
|
};
|
|
74
82
|
error: boolean;
|
|
75
83
|
}>;
|
|
@@ -98,3 +106,4 @@ export function parseStartpageResults(html: any): {
|
|
|
98
106
|
}[];
|
|
99
107
|
export function canonicalizeSearchUrl(value: any, base: any): string;
|
|
100
108
|
export function mergeRankedResults(rankedLists: any, limit?: number): any[];
|
|
109
|
+
import { searchCodexSubscription } from "./codex-subscription-search.js";
|
package/types/ai/types.d.ts
CHANGED
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
* @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Per-run ACP profile resolver; wins over the host default.
|
|
248
248
|
* @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Per-run ACP permission/elicitation callback; wins over the host default.
|
|
249
249
|
* @property {Uint8Array} [acpSessionTokenKey] Host-owned 32-byte key for confidential authenticated ACP session handles. Required for every ACP task run.
|
|
250
|
-
* @property {{backend?: "auto"|"searxng"|"keyless", endpoint?: string}} [webSearchConfig] Run-scoped WebSearch backend configuration.
|
|
250
|
+
* @property {{backend?: "auto"|"searxng"|"codex"|"keyless", endpoint?: string, codex?: {model?: string}}} [webSearchConfig] Run-scoped WebSearch backend configuration.
|
|
251
251
|
* @property {{render?: "never"|"auto", browserCommand?: string}} [webFetchConfig] Run-scoped WebFetch extraction/render configuration.
|
|
252
252
|
* @property {"sequential"|"safe-parallel"} [piToolExecutionMode] Pi built-in tool scheduling mode. Safe parallelism is the default.
|
|
253
253
|
* @property {"one-at-a-time"|"all"} [piToolParallelismMode] DEPRECATED. Compatibility alias mapped to piToolExecutionMode.
|
|
@@ -941,8 +941,11 @@ export type RuntimeRunOptions = {
|
|
|
941
941
|
* Run-scoped WebSearch backend configuration.
|
|
942
942
|
*/
|
|
943
943
|
webSearchConfig?: {
|
|
944
|
-
backend?: "auto" | "searxng" | "keyless";
|
|
944
|
+
backend?: "auto" | "searxng" | "codex" | "keyless";
|
|
945
945
|
endpoint?: string;
|
|
946
|
+
codex?: {
|
|
947
|
+
model?: string;
|
|
948
|
+
};
|
|
946
949
|
};
|
|
947
950
|
/**
|
|
948
951
|
* Run-scoped WebFetch extraction/render configuration.
|