@mono-agent/agent-runtime 0.20.11 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +288 -26
- package/README.md +352 -477
- package/package.json +13 -44
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +108 -9
- package/src/agent/tools/bash.js +11 -26
- package/src/agent/tools/codex-subscription-search.js +123 -29
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +149 -0
- package/src/agent/tools/pi-bridge.js +123 -19
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +293 -0
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +26 -6
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/cost.js +13 -68
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +5 -17
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +221 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +94 -42
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +376 -0
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +38 -14
- package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
- package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +279 -28
- package/src/ai/providers/pi-native.js +206 -61
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +76 -417
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +37 -112
- package/src/index.js +0 -6
- package/src/runtime.js +29 -16
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -2
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +7 -3
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +47 -0
- package/types/agent/tools/pi-bridge.d.ts +7 -4
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +98 -0
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +14 -4
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +2 -4
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +81 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +162 -256
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -4,12 +4,14 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
-
import { createCodexAppServerClient } from "../../ai/providers/codex-
|
|
7
|
+
import { createCodexAppServerClient } from "../../ai/providers/codex/app-server-client.js";
|
|
8
|
+
import { boundWebSearchSnippet, sliceWellFormedCodePoints, toWellFormedText } from "./web-search-output.js";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_CODEX_SEARCH_MODEL = "gpt-5.6-luna";
|
|
10
11
|
|
|
11
12
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
12
|
-
const IDLE_CLOSE_MS =
|
|
13
|
+
const IDLE_CLOSE_MS = 30_000;
|
|
14
|
+
let quotaSnapshot;
|
|
13
15
|
const MAX_MODEL_PAGES = 10;
|
|
14
16
|
const MAX_RESULTS = 100;
|
|
15
17
|
const SEARCH_ONLY_INSTRUCTIONS = [
|
|
@@ -59,16 +61,23 @@ export async function inspectCodexSubscriptionSearch(options = {}) {
|
|
|
59
61
|
* turns or cross-wire app-server notifications between requests.
|
|
60
62
|
*
|
|
61
63
|
* @param {string} query
|
|
62
|
-
* @param {{model?: string, signal?: AbortSignal, clientFactory?: typeof createCodexAppServerClient}} [options]
|
|
64
|
+
* @param {{model?: string, signal?: AbortSignal, clientFactory?: typeof createCodexAppServerClient, coordinator?: any, language?: string, timeRange?: string, claimRequest?: () => void}} [options]
|
|
63
65
|
*/
|
|
64
66
|
export function searchCodexSubscription(query, options = {}) {
|
|
65
|
-
|
|
67
|
+
let executing = false;
|
|
68
|
+
const pending = enqueue(async () => {
|
|
69
|
+
executing = true;
|
|
66
70
|
if (options.signal?.aborted) return abortedResult();
|
|
67
71
|
const model = normalizeModel(options.model);
|
|
68
72
|
let current;
|
|
69
73
|
try {
|
|
70
|
-
current = await getBroker(model, options.clientFactory);
|
|
71
|
-
|
|
74
|
+
current = await getBroker(model, options.clientFactory, options.signal);
|
|
75
|
+
options.signal?.throwIfAborted();
|
|
76
|
+
await checkQuota(current, options.coordinator, options.signal);
|
|
77
|
+
options.signal?.throwIfAborted();
|
|
78
|
+
const quotaBefore = quotaSnapshot?.checkedAt;
|
|
79
|
+
const result = await runSearch(current, query, model, options.signal, options);
|
|
80
|
+
if (quotaSnapshot && quotaSnapshot.checkedAt !== quotaBefore) await options.coordinator?.writeQuota(quotaSnapshot.value);
|
|
72
81
|
scheduleIdleClose();
|
|
73
82
|
return result;
|
|
74
83
|
} catch (error) {
|
|
@@ -78,9 +87,70 @@ export function searchCodexSubscription(query, options = {}) {
|
|
|
78
87
|
backend: "codex",
|
|
79
88
|
message: `Codex subscription search unavailable: ${publicReason(error)}`,
|
|
80
89
|
retryable: isRetryable(error),
|
|
90
|
+
code: options.signal?.aborted ? (options.signal.reason?.code === "deadline_exceeded" ? "deadline_exceeded" : "aborted") : error?.code,
|
|
91
|
+
quotaSkipped: ["quota_reserved", "quota_unavailable"].includes(error?.code),
|
|
92
|
+
retryAfterMs: error?.retryAfterMs,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
});
|
|
96
|
+
return abortable(pending, options.signal, () => executing);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function abortable(pending, signal, executing) {
|
|
100
|
+
if (!signal) return pending;
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const aborted = () => { if (!executing()) reject(signal.reason || Object.assign(new Error("WebSearch was aborted."), { name: "AbortError" })); };
|
|
103
|
+
if (signal.aborted) aborted();
|
|
104
|
+
else signal.addEventListener("abort", aborted, { once: true });
|
|
105
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function checkQuota(current, coordinator, signal) {
|
|
110
|
+
const shared = coordinator ? await coordinator.readQuota() : quotaSnapshot;
|
|
111
|
+
let snapshot = shared;
|
|
112
|
+
if (!snapshot || Date.now() - snapshot.checkedAt > 60_000 || snapshot.checkedAt > Date.now()) {
|
|
113
|
+
let response;
|
|
114
|
+
try { response = await searchRequest(current.client, "account/rateLimits/read", {}, { timeoutMs: 5000 }, signal); }
|
|
115
|
+
catch { throw Object.assign(new Error("Codex quota information is unavailable."), { code: "quota_unavailable" }); }
|
|
116
|
+
snapshot = { checkedAt: Date.now(), value: quotaValue(response) };
|
|
117
|
+
quotaSnapshot = snapshot;
|
|
118
|
+
await coordinator?.writeQuota(snapshot.value);
|
|
119
|
+
}
|
|
120
|
+
quotaSnapshot = snapshot;
|
|
121
|
+
const windows = snapshot.value?.windows;
|
|
122
|
+
if (!Array.isArray(windows) || windows.length === 0 || !windows.every((w) =>
|
|
123
|
+
Number.isFinite(w.usedPercent) && w.usedPercent >= 0 && w.usedPercent <= 100
|
|
124
|
+
&& Number.isSafeInteger(w.resetsAt) && w.resetsAt * 1000 > Date.now())) {
|
|
125
|
+
throw Object.assign(new Error("Codex quota information is unavailable."), { code: "quota_unavailable" });
|
|
126
|
+
}
|
|
127
|
+
const reserved = windows.filter((w) => w.usedPercent >= 90);
|
|
128
|
+
if (reserved.length) throw Object.assign(new Error("Codex search preserves the remaining subscription allowance."), {
|
|
129
|
+
code: "quota_reserved", retryAfterMs: Math.max(...reserved.map((w) => w.resetsAt * 1000 - Date.now())),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function quotaValue(response) {
|
|
134
|
+
const bucket = response?.rateLimitsByLimitId?.codex ?? response?.rateLimits;
|
|
135
|
+
return { windows: [bucket?.primary, bucket?.secondary].filter(Boolean).map((w) => ({ usedPercent: w.usedPercent, resetsAt: w.resetsAt })) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Close the transport before releasing admission on abort, including startup
|
|
139
|
+
// and thread/start. A queued or late request cannot launch a new search turn.
|
|
140
|
+
async function searchRequest(client, method, params, options, signal) {
|
|
141
|
+
signal?.throwIfAborted();
|
|
142
|
+
if (!signal) return await client.request(method, params, options);
|
|
143
|
+
let abort;
|
|
144
|
+
const cancelled = new Promise((_, reject) => {
|
|
145
|
+
abort = () => {
|
|
146
|
+
Promise.resolve(client.close()).then(
|
|
147
|
+
() => reject(signal.reason), () => reject(signal.reason),
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
151
|
+
});
|
|
152
|
+
try { return await Promise.race([client.request(method, params, options), cancelled]); }
|
|
153
|
+
finally { signal.removeEventListener("abort", abort); }
|
|
84
154
|
}
|
|
85
155
|
|
|
86
156
|
function enqueue(task) {
|
|
@@ -89,14 +159,16 @@ function enqueue(task) {
|
|
|
89
159
|
return result;
|
|
90
160
|
}
|
|
91
161
|
|
|
92
|
-
async function getBroker(model, clientFactory) {
|
|
162
|
+
async function getBroker(model, clientFactory, signal) {
|
|
93
163
|
clearIdleTimer();
|
|
94
164
|
if (broker?.models.has(model)) return broker;
|
|
95
165
|
if (broker && !broker.models.has(model)) await closeBroker();
|
|
96
166
|
if (!brokerOpening) {
|
|
97
167
|
brokerOpening = (async () => {
|
|
98
|
-
const owned = await openBroker(clientFactory);
|
|
99
|
-
|
|
168
|
+
const owned = await openBroker(clientFactory, signal);
|
|
169
|
+
let ready;
|
|
170
|
+
try { ready = await inspectClient(owned.client, model, signal); }
|
|
171
|
+
catch (error) { await closeOwnedBroker(owned); throw error; }
|
|
100
172
|
if (!ready.ok) {
|
|
101
173
|
await closeOwnedBroker(owned);
|
|
102
174
|
throw new Error(ready.reason);
|
|
@@ -109,7 +181,7 @@ async function getBroker(model, clientFactory) {
|
|
|
109
181
|
return await brokerOpening;
|
|
110
182
|
}
|
|
111
183
|
|
|
112
|
-
async function openBroker(clientFactory = createCodexAppServerClient) {
|
|
184
|
+
async function openBroker(clientFactory = createCodexAppServerClient, signal) {
|
|
113
185
|
const directory = await mkdtemp(join(tmpdir(), "mono-agent-codex-search-"));
|
|
114
186
|
/** @type {{handler: (message: any) => void}} */
|
|
115
187
|
const target = { handler: () => {} };
|
|
@@ -117,16 +189,19 @@ async function openBroker(clientFactory = createCodexAppServerClient) {
|
|
|
117
189
|
try {
|
|
118
190
|
client = clientFactory({
|
|
119
191
|
cwd: directory,
|
|
120
|
-
onNotification: (message) =>
|
|
192
|
+
onNotification: (message) => {
|
|
193
|
+
if (message?.method === "account/rateLimits/updated") quotaSnapshot = { checkedAt: Date.now(), value: quotaValue(message.params) };
|
|
194
|
+
target.handler(message);
|
|
195
|
+
},
|
|
121
196
|
onServerRequest: (message) => {
|
|
122
197
|
target.handler(message);
|
|
123
198
|
throw new Error("Codex subscription search rejected an unexpected server request.");
|
|
124
199
|
},
|
|
125
200
|
});
|
|
126
|
-
await client
|
|
201
|
+
await searchRequest(client, "initialize", {
|
|
127
202
|
clientInfo: { name: "mono-agent-web-search", title: "mono-agent WebSearch", version: "0" },
|
|
128
203
|
capabilities: { experimentalApi: true },
|
|
129
|
-
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
204
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS }, signal);
|
|
130
205
|
return { client, directory, models: new Set(), target };
|
|
131
206
|
} catch (error) {
|
|
132
207
|
await Promise.resolve(client?.close?.()).catch(() => {});
|
|
@@ -135,8 +210,8 @@ async function openBroker(clientFactory = createCodexAppServerClient) {
|
|
|
135
210
|
}
|
|
136
211
|
}
|
|
137
212
|
|
|
138
|
-
async function inspectClient(client, model) {
|
|
139
|
-
const account = await client
|
|
213
|
+
async function inspectClient(client, model, signal) {
|
|
214
|
+
const account = await searchRequest(client, "account/read", { refreshToken: false }, { timeoutMs: REQUEST_TIMEOUT_MS }, signal);
|
|
140
215
|
if (account?.account?.type !== "chatgpt") {
|
|
141
216
|
return {
|
|
142
217
|
ok: false,
|
|
@@ -146,10 +221,10 @@ async function inspectClient(client, model) {
|
|
|
146
221
|
models: new Set(),
|
|
147
222
|
};
|
|
148
223
|
}
|
|
149
|
-
const capabilities = await client
|
|
224
|
+
const capabilities = await searchRequest(client,
|
|
150
225
|
"modelProvider/capabilities/read",
|
|
151
226
|
{},
|
|
152
|
-
{ timeoutMs: REQUEST_TIMEOUT_MS },
|
|
227
|
+
{ timeoutMs: REQUEST_TIMEOUT_MS }, signal,
|
|
153
228
|
);
|
|
154
229
|
if (capabilities?.webSearch !== true) {
|
|
155
230
|
return {
|
|
@@ -160,7 +235,7 @@ async function inspectClient(client, model) {
|
|
|
160
235
|
models: new Set(),
|
|
161
236
|
};
|
|
162
237
|
}
|
|
163
|
-
const models = await readModels(client);
|
|
238
|
+
const models = await readModels(client, signal);
|
|
164
239
|
if (!models.has(model)) {
|
|
165
240
|
return {
|
|
166
241
|
ok: false,
|
|
@@ -179,15 +254,15 @@ async function inspectClient(client, model) {
|
|
|
179
254
|
};
|
|
180
255
|
}
|
|
181
256
|
|
|
182
|
-
async function readModels(client) {
|
|
257
|
+
async function readModels(client, signal) {
|
|
183
258
|
const models = new Set();
|
|
184
259
|
let cursor = null;
|
|
185
260
|
for (let page = 0; page < MAX_MODEL_PAGES; page += 1) {
|
|
186
|
-
const response = await client
|
|
261
|
+
const response = await searchRequest(client, "model/list", {
|
|
187
262
|
includeHidden: false,
|
|
188
263
|
limit: 100,
|
|
189
264
|
...(cursor === null ? {} : { cursor }),
|
|
190
|
-
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
265
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS }, signal);
|
|
191
266
|
if (!Array.isArray(response?.data)) throw new Error("Codex returned an invalid model catalog.");
|
|
192
267
|
for (const row of response.data) {
|
|
193
268
|
if (typeof row?.id === "string" && row.id.trim()) models.add(row.id.trim());
|
|
@@ -198,7 +273,8 @@ async function readModels(client) {
|
|
|
198
273
|
throw new Error("Codex model catalog exceeded the pagination bound.");
|
|
199
274
|
}
|
|
200
275
|
|
|
201
|
-
async function runSearch(current, query, model, signal) {
|
|
276
|
+
async function runSearch(current, query, model, signal, preferences = {}) {
|
|
277
|
+
signal?.throwIfAborted();
|
|
202
278
|
const state = /** @type {any} */ ({
|
|
203
279
|
threadId: "",
|
|
204
280
|
turnId: "",
|
|
@@ -221,7 +297,7 @@ async function runSearch(current, query, model, signal) {
|
|
|
221
297
|
};
|
|
222
298
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
223
299
|
try {
|
|
224
|
-
const thread = await current.client
|
|
300
|
+
const thread = await searchRequest(current.client, "thread/start", {
|
|
225
301
|
model,
|
|
226
302
|
modelProvider: "openai",
|
|
227
303
|
allowProviderModelFallback: false,
|
|
@@ -234,17 +310,21 @@ async function runSearch(current, query, model, signal) {
|
|
|
234
310
|
project_doc_max_bytes: 0,
|
|
235
311
|
mcp_servers: {},
|
|
236
312
|
},
|
|
237
|
-
developerInstructions: SEARCH_ONLY_INSTRUCTIONS
|
|
313
|
+
developerInstructions: SEARCH_ONLY_INSTRUCTIONS + (
|
|
314
|
+
preferences.language || preferences.timeRange
|
|
315
|
+
? ` Search preferences (keep query text unchanged): language=${JSON.stringify(preferences.language || "default")}; time range=${JSON.stringify(preferences.timeRange || "any")}. Use supported search filters; do not invent dates.` : ""
|
|
316
|
+
),
|
|
238
317
|
ephemeral: true,
|
|
239
318
|
sessionStartSource: "startup",
|
|
240
319
|
environments: [],
|
|
241
320
|
dynamicTools: [],
|
|
242
321
|
selectedCapabilityRoots: [],
|
|
243
322
|
experimentalRawEvents: false,
|
|
244
|
-
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
323
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS }, signal);
|
|
245
324
|
state.threadId = thread?.thread?.id || "";
|
|
246
325
|
if (!state.threadId) throw new Error("Codex did not return a search thread id.");
|
|
247
|
-
|
|
326
|
+
preferences.claimRequest?.();
|
|
327
|
+
const turn = await searchRequest(current.client, "turn/start", {
|
|
248
328
|
threadId: state.threadId,
|
|
249
329
|
input: [{ type: "text", text: String(query), text_elements: [] }],
|
|
250
330
|
cwd: current.directory,
|
|
@@ -255,7 +335,7 @@ async function runSearch(current, query, model, signal) {
|
|
|
255
335
|
effort: "low",
|
|
256
336
|
summary: "none",
|
|
257
337
|
environments: [],
|
|
258
|
-
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
338
|
+
}, { timeoutMs: REQUEST_TIMEOUT_MS }, signal);
|
|
259
339
|
state.turnId = turn?.turn?.id || state.turnId;
|
|
260
340
|
if (state.violation && state.turnId) {
|
|
261
341
|
await current.client.request("turn/interrupt", {
|
|
@@ -276,6 +356,9 @@ async function runSearch(current, query, model, signal) {
|
|
|
276
356
|
if (actualQuery !== String(query)) {
|
|
277
357
|
throw new Error("Codex changed the exact web search query.");
|
|
278
358
|
}
|
|
359
|
+
if (!Array.isArray(item.results) || item.results.some((row) => !row || typeof row.url !== "string" || !validResultUrl(row.url))) {
|
|
360
|
+
throw new Error("Codex returned malformed structured search results.");
|
|
361
|
+
}
|
|
279
362
|
const results = normalizeResults(item.results);
|
|
280
363
|
return {
|
|
281
364
|
ok: true,
|
|
@@ -329,15 +412,24 @@ function handleNotification(message, state, client) {
|
|
|
329
412
|
}
|
|
330
413
|
}
|
|
331
414
|
|
|
415
|
+
function validResultUrl(value) {
|
|
416
|
+
try {
|
|
417
|
+
const url = new URL(value);
|
|
418
|
+
return ["http:", "https:"].includes(url.protocol) && Boolean(url.hostname) && !url.username && !url.password;
|
|
419
|
+
} catch { return false; }
|
|
420
|
+
}
|
|
421
|
+
|
|
332
422
|
function normalizeResults(rows) {
|
|
333
423
|
if (!Array.isArray(rows)) return [];
|
|
334
424
|
const results = [];
|
|
335
425
|
for (const row of rows) {
|
|
336
426
|
if (!row || typeof row !== "object" || typeof row.url !== "string") continue;
|
|
427
|
+
const snippet = boundWebSearchSnippet(row.snippet);
|
|
337
428
|
results.push({
|
|
338
429
|
title: boundedText(row.title, 500),
|
|
339
430
|
url: row.url,
|
|
340
|
-
snippet:
|
|
431
|
+
snippet: snippet.text,
|
|
432
|
+
snippetTruncated: snippet.truncated,
|
|
341
433
|
provenance: boundedText(row.domain || row.ref_id || row.type, 300),
|
|
342
434
|
backend: "codex",
|
|
343
435
|
});
|
|
@@ -391,7 +483,8 @@ function normalizeModel(value) {
|
|
|
391
483
|
}
|
|
392
484
|
|
|
393
485
|
function boundedText(value, max) {
|
|
394
|
-
|
|
486
|
+
const text = typeof value === "string" ? toWellFormedText(value).replace(/\s+/gu, " ").trim() : "";
|
|
487
|
+
return sliceWellFormedCodePoints(text, max);
|
|
395
488
|
}
|
|
396
489
|
|
|
397
490
|
function safeReason(error) {
|
|
@@ -430,6 +523,7 @@ function abortedResult() {
|
|
|
430
523
|
|
|
431
524
|
/** Test hook for process-shared broker state. */
|
|
432
525
|
export async function __resetCodexSubscriptionSearchForTests() {
|
|
526
|
+
quotaSnapshot = undefined;
|
|
433
527
|
await enqueue(async () => { await closeBroker(); });
|
|
434
528
|
brokerOpening = null;
|
|
435
529
|
}
|
package/src/agent/tools/exec.js
CHANGED
|
@@ -25,7 +25,7 @@ const MAX_EXEC_ARGS = 256;
|
|
|
25
25
|
/** @typedef {import("./shared/process-jobs.js").ProcessJobsController} ProcessJobsController */
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @param {{executable: string, args?: string[], workdir?: string, description?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean}} params
|
|
28
|
+
* @param {{executable: string, args?: string[], workdir?: string, description?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean, wake_on_completion?: boolean}} params
|
|
29
29
|
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: ProcessJobsController}} [options]
|
|
30
30
|
*/
|
|
31
31
|
export async function execToolImpl(params, options = {}) {
|
|
@@ -35,7 +35,7 @@ export async function execToolImpl(params, options = {}) {
|
|
|
35
35
|
/**
|
|
36
36
|
* Execute an argv vector directly, without shell parsing.
|
|
37
37
|
*
|
|
38
|
-
* @param {{executable: string, args?: string[], workdir?: string, description?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean}} params
|
|
38
|
+
* @param {{executable: string, args?: string[], workdir?: string, description?: string, timeout_ms?: number, max_output_chars?: number, background?: boolean, wake_on_completion?: boolean}} params
|
|
39
39
|
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: ProcessJobsController}} [options]
|
|
40
40
|
*/
|
|
41
41
|
export async function execToolRun(
|
|
@@ -47,6 +47,7 @@ export async function execToolRun(
|
|
|
47
47
|
timeout_ms,
|
|
48
48
|
max_output_chars,
|
|
49
49
|
background,
|
|
50
|
+
wake_on_completion,
|
|
50
51
|
},
|
|
51
52
|
{
|
|
52
53
|
signal,
|
|
@@ -57,6 +58,12 @@ export async function execToolRun(
|
|
|
57
58
|
} = {},
|
|
58
59
|
) {
|
|
59
60
|
const startedAt = Date.now();
|
|
61
|
+
if (wake_on_completion !== undefined && (background !== true || typeof wake_on_completion !== "boolean")) {
|
|
62
|
+
return failed("Error: wake_on_completion requires background=true and a boolean value.", "process_job_invalid", startedAt);
|
|
63
|
+
}
|
|
64
|
+
if (background === true && !processJobsController) {
|
|
65
|
+
return failed("Error: Background process jobs are unavailable for this request.", "background_unsupported", startedAt);
|
|
66
|
+
}
|
|
60
67
|
const executableProblem = validateExecutable(executable);
|
|
61
68
|
if (executableProblem) return failed(executableProblem, "invalid_executable", startedAt);
|
|
62
69
|
const argsProblem = validateArgs(args);
|
|
@@ -103,6 +110,7 @@ export async function execToolRun(
|
|
|
103
110
|
prepared,
|
|
104
111
|
summary: `Exec command (${args.length} argument${args.length === 1 ? "" : "s"}; values redacted)`,
|
|
105
112
|
description,
|
|
113
|
+
wakeOnCompletion: wake_on_completion,
|
|
106
114
|
// Re-derived from the raw param: `timeoutMs` carries the foreground
|
|
107
115
|
// ceiling, and a background job is bounded by processJobs instead.
|
|
108
116
|
timeoutMs: timeout_ms === undefined ? undefined : normalizeBackgroundTimeoutMs(timeout_ms),
|
package/src/agent/tools/index.js
CHANGED
|
@@ -18,6 +18,13 @@ export {
|
|
|
18
18
|
normalizeProcessTimeoutMs,
|
|
19
19
|
} from "./bash.js";
|
|
20
20
|
export { execToolImpl, execToolRun } from "./exec.js";
|
|
21
|
+
export {
|
|
22
|
+
DEFAULT_MONITOR_TIMEOUT_MS,
|
|
23
|
+
MIN_MONITOR_TIMEOUT_MS,
|
|
24
|
+
monitorStopToolRun,
|
|
25
|
+
monitorToolRun,
|
|
26
|
+
normalizeMonitorTimeoutMs,
|
|
27
|
+
} from "./monitor.js";
|
|
21
28
|
export { webFetchToolImpl, performWebFetch } from "./web-fetch.js";
|
|
22
29
|
export { webSearchToolImpl, performWebSearch } from "./web-search.js";
|
|
23
30
|
export {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
5
|
+
import {
|
|
6
|
+
isPathAllowed,
|
|
7
|
+
isWorkdirAllowed,
|
|
8
|
+
workspaceRoot,
|
|
9
|
+
} from "./shared/path-resolver.js";
|
|
10
|
+
import { handOffMonitor, handOffMonitorStop } from "./shared/monitors.js";
|
|
11
|
+
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
12
|
+
import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
13
|
+
import { cleanBashEnvironment } from "./shared/bash-environment.js";
|
|
14
|
+
|
|
15
|
+
/** Claude-Code-compatible defaults: 5 minutes, never below one second. */
|
|
16
|
+
export const DEFAULT_MONITOR_TIMEOUT_MS = 300_000;
|
|
17
|
+
export const MIN_MONITOR_TIMEOUT_MS = 1_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize `timeout_ms` for a monitor. Unlike Bash there is no
|
|
21
|
+
* seconds-vs-milliseconds heuristic — this field is new, so it is exact
|
|
22
|
+
* milliseconds only. `undefined`/invalid falls back to the documented default;
|
|
23
|
+
* the host's own ceiling clamps the result on its side and reports what it
|
|
24
|
+
* actually granted.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeMonitorTimeoutMs(value, fallback = DEFAULT_MONITOR_TIMEOUT_MS) {
|
|
27
|
+
const number = Number(value);
|
|
28
|
+
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
29
|
+
return Math.max(MIN_MONITOR_TIMEOUT_MS, Math.floor(number));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Structured Monitor execution used by the Pi bridge.
|
|
34
|
+
*
|
|
35
|
+
* Command preparation is deliberately byte-identical to Bash: the same
|
|
36
|
+
* `/bin/bash --noprofile --norc -c` shape, the same workdir rules, the same
|
|
37
|
+
* cleaned startup environment, and the same sandbox `prepareCommand` seam. A
|
|
38
|
+
* monitor must never be a way to run a command Bash could not.
|
|
39
|
+
*
|
|
40
|
+
* @param {{command?: string, description?: string, timeout_ms?: number, persistent?: boolean, workdir?: string, wake_on?: "batch"|"exit", dedupe?: "none"|"batch", min_wake_interval_ms?: number}} params
|
|
41
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, monitorsController?: import("./shared/monitors.js").MonitorsController}} [options]
|
|
42
|
+
*/
|
|
43
|
+
export async function monitorToolRun(
|
|
44
|
+
{ command, description, timeout_ms, persistent, workdir, wake_on = "batch", dedupe = "none", min_wake_interval_ms = 0 },
|
|
45
|
+
{ sandboxPolicy, sandboxEngine, ctx, monitorsController } = {},
|
|
46
|
+
) {
|
|
47
|
+
const startedAt = Date.now();
|
|
48
|
+
if (!monitorsController) {
|
|
49
|
+
return failed("Error: Monitors are unsupported for this tool call.", "monitor_unsupported", startedAt);
|
|
50
|
+
}
|
|
51
|
+
if (typeof command !== "string") {
|
|
52
|
+
return failed("Error: Monitor command must be a string.", "monitor_invalid", startedAt);
|
|
53
|
+
}
|
|
54
|
+
if (command.includes("\0")) {
|
|
55
|
+
return failed("Error: Monitor command must not contain NUL characters.", "monitor_invalid", startedAt);
|
|
56
|
+
}
|
|
57
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
58
|
+
return failed("Error: Monitor description is required.", "monitor_invalid", startedAt);
|
|
59
|
+
}
|
|
60
|
+
if (!["batch", "exit"].includes(wake_on)
|
|
61
|
+
|| !["none", "batch"].includes(dedupe)
|
|
62
|
+
|| !Number.isSafeInteger(min_wake_interval_ms) || min_wake_interval_ms < 0
|
|
63
|
+
|| (wake_on === "exit" && (dedupe !== "none" || min_wake_interval_ms !== 0))) {
|
|
64
|
+
return failed("Error: Invalid Monitor wake policy; exit-only requires dedupe none and interval 0.", "monitor_invalid", startedAt);
|
|
65
|
+
}
|
|
66
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
67
|
+
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
68
|
+
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
69
|
+
const pathOptions = { sandboxPolicy: policy, ctx: resolvedCtx };
|
|
70
|
+
if (workdir && !isWorkdirAllowed(workdir, pathOptions)) {
|
|
71
|
+
return failed(`Error: Working directory not allowed: ${workdir}`, "workdir_denied", startedAt);
|
|
72
|
+
}
|
|
73
|
+
const cwd = workspaceRoot(workdir, resolvedCtx);
|
|
74
|
+
if (!isPathAllowed(cwd, workdir, pathOptions)) {
|
|
75
|
+
return failed(`Error: Working directory not allowed: ${cwd}`, "workdir_denied", startedAt);
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync(cwd)) {
|
|
78
|
+
return failed(`Error: Working directory not found: ${cwd}`, "workdir_not_found", startedAt);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let prepared;
|
|
82
|
+
try {
|
|
83
|
+
prepared = await sandbox.prepareCommand({
|
|
84
|
+
policy,
|
|
85
|
+
engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
|
|
86
|
+
command: {
|
|
87
|
+
command: "/bin/bash",
|
|
88
|
+
args: ["--noprofile", "--norc", "-c", command],
|
|
89
|
+
cwd,
|
|
90
|
+
env: requestToolProcessEnvironment(resolvedCtx, cleanBashEnvironment()),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return failed(`Error: ${error?.message || String(error)}`, "sandbox_prepare_failed", startedAt);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isPersistent = persistent === true;
|
|
98
|
+
return await handOffMonitor({
|
|
99
|
+
controller: monitorsController,
|
|
100
|
+
prepared,
|
|
101
|
+
summary: `Monitor command (${command.length} characters; content redacted)`,
|
|
102
|
+
description,
|
|
103
|
+
// A persistent monitor has no timed deadline; sending one anyway would let
|
|
104
|
+
// an ignored field look honoured in the durable record.
|
|
105
|
+
...(isPersistent ? {} : { timeoutMs: normalizeMonitorTimeoutMs(timeout_ms) }),
|
|
106
|
+
persistent: isPersistent,
|
|
107
|
+
wakeOn: wake_on,
|
|
108
|
+
dedupe,
|
|
109
|
+
minWakeIntervalMs: min_wake_interval_ms,
|
|
110
|
+
startedAt,
|
|
111
|
+
failed,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{monitor_id?: string}} params
|
|
117
|
+
* @param {{monitorsController?: import("./shared/monitors.js").MonitorsController}} [options]
|
|
118
|
+
*/
|
|
119
|
+
export async function monitorStopToolRun({ monitor_id }, { monitorsController } = {}) {
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
if (!monitorsController) {
|
|
122
|
+
return failed("Error: Monitors are unsupported for this tool call.", "monitor_unsupported", startedAt);
|
|
123
|
+
}
|
|
124
|
+
return await handOffMonitorStop({
|
|
125
|
+
controller: monitorsController,
|
|
126
|
+
monitorId: monitor_id,
|
|
127
|
+
startedAt,
|
|
128
|
+
failed,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function failed(text, code, startedAt) {
|
|
133
|
+
return {
|
|
134
|
+
text,
|
|
135
|
+
outcome: {
|
|
136
|
+
status: "error",
|
|
137
|
+
code,
|
|
138
|
+
retryable: false,
|
|
139
|
+
attempts: 1,
|
|
140
|
+
durationMs: Date.now() - startedAt,
|
|
141
|
+
bytes: 0,
|
|
142
|
+
truncated: false,
|
|
143
|
+
exitCode: null,
|
|
144
|
+
signal: null,
|
|
145
|
+
timedOut: false,
|
|
146
|
+
},
|
|
147
|
+
error: true,
|
|
148
|
+
};
|
|
149
|
+
}
|