@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 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/keyless WebSearch backend selection. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -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
+ }
@@ -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 with an operator-owned SearXNG endpoint when available, otherwise a keyless fallback, and return deduplicated ranked results.", objectSchema({
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 and/or the keyless HTML
85
- * fallback chain. Returns a structured internal outcome for the Pi bridge.
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 includeDomains = normalizeDomains(domains);
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, includeDomains),
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
- anyProviderSucceeded = true;
151
- providersUsed.add(result.backend);
152
- rankedLists.push(filterByDomains(result.results, includeDomains, excludeDomains));
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 initialResults = await Promise.all(initialQueries.map(runQuery));
159
- initialResults.forEach(recordResult);
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 ? [...providersUsed][0] : "mixed";
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" || (config.backend === "auto" && config.endpoint)) {
296
+ if (config.backend === "searxng") {
247
297
  const result = await searchSearxng(query, options);
248
- // Strict mode has nothing to fall through to, so its answer stands as-is.
249
- if (config.backend === "searxng") return { ...result, failures };
250
- if (result.ok && result.results.length > 0) return { ...result, failures };
251
- if (result.ok) emptySuccess = result;
252
- else failures.push(result);
253
- if (options.signal?.aborted) return abortedSearch(result.backend, failures);
254
- }
255
- if (config.backend === "keyless" || config.backend === "auto") {
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
- return { backend, endpoint };
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 relaxedQuery(query) {
759
- const relaxed = query
760
- .replace(/"([^"]+)"/gu, "$1")
761
- .replace(/\bsite:\S+/giu, " ")
762
- .replace(/\s+/gu, " ")
763
- .trim();
764
- return relaxed && relaxed !== query ? relaxed : null;
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 and/or the keyless HTML
24
- * fallback chain. Returns a structured internal outcome for the Pi bridge.
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";
@@ -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.