@mono-agent/agent-runtime 0.1.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 +219 -0
- package/LICENSE +674 -0
- package/README.md +430 -0
- package/package.json +46 -0
- package/src/agent/allowlists.js +49 -0
- package/src/agent/approval.js +211 -0
- package/src/agent/compaction.js +752 -0
- package/src/agent/index.js +40 -0
- package/src/agent/prompt/skill-index.js +66 -0
- package/src/agent/tool-bloat.js +164 -0
- package/src/agent/tools/bash.js +156 -0
- package/src/agent/tools/edit.js +15 -0
- package/src/agent/tools/glob.js +71 -0
- package/src/agent/tools/grep.js +84 -0
- package/src/agent/tools/index.js +17 -0
- package/src/agent/tools/pi-bridge.js +638 -0
- package/src/agent/tools/read.js +39 -0
- package/src/agent/tools/shared/constants.js +21 -0
- package/src/agent/tools/shared/dedup.js +31 -0
- package/src/agent/tools/shared/output-truncation.js +54 -0
- package/src/agent/tools/shared/path-resolver.js +156 -0
- package/src/agent/tools/shared/ripgrep.js +130 -0
- package/src/agent/tools/shared/runtime-context.js +69 -0
- package/src/agent/tools/web-fetch.js +59 -0
- package/src/agent/tools/web-search.js +21 -0
- package/src/agent/tools/write.js +14 -0
- package/src/agent/transcript.js +227 -0
- package/src/ai/backend.js +17 -0
- package/src/ai/cost.js +164 -0
- package/src/ai/failure.js +165 -0
- package/src/ai/file-change-stats.js +234 -0
- package/src/ai/index.js +16 -0
- package/src/ai/live-input-prompt.js +15 -0
- package/src/ai/observer.js +233 -0
- package/src/ai/providers/claude-cli.js +694 -0
- package/src/ai/providers/claude-sdk.js +864 -0
- package/src/ai/providers/claude-subagents.js +67 -0
- package/src/ai/providers/codex-app.js +1045 -0
- package/src/ai/providers/opencode-app.js +356 -0
- package/src/ai/providers/opencode-discovery.js +39 -0
- package/src/ai/providers/pi-events.js +62 -0
- package/src/ai/providers/pi-messages.js +68 -0
- package/src/ai/providers/pi-models.js +111 -0
- package/src/ai/providers/pi-sdk.js +1310 -0
- package/src/ai/registry.js +5 -0
- package/src/ai/runtime/capabilities-used.js +56 -0
- package/src/ai/runtime/capabilities.js +44 -0
- package/src/ai/runtime/context-windows.js +38 -0
- package/src/ai/runtime/fast-mode.js +8 -0
- package/src/ai/runtime/model-refs.js +144 -0
- package/src/ai/runtime/registry.js +57 -0
- package/src/ai/runtime/router.js +214 -0
- package/src/ai/runtime/sessions.js +126 -0
- package/src/ai/streaming/codex-events.js +139 -0
- package/src/ai/streaming/opencode-events.js +54 -0
- package/src/ai/types.js +70 -0
- package/src/index.js +23 -0
- package/src/pi-auth.js +80 -0
- package/src/runtime-brand.js +32 -0
- package/src/runtime.js +104 -0
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { normalizeCodexItemEvent } from "../streaming/codex-events.js";
|
|
4
|
+
import { createFileChangePayload } from "../file-change-stats.js";
|
|
5
|
+
import { formatLiveInputGuidance } from "../live-input-prompt.js";
|
|
6
|
+
import { estimateCost } from "../cost.js";
|
|
7
|
+
import { codexModelSupportsFastMode, normalizeFastMode } from "../runtime/fast-mode.js";
|
|
8
|
+
import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
|
|
9
|
+
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
10
|
+
import { createSessionRegistry } from "../runtime/sessions.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
13
|
+
const DEFAULT_THREAD_START_ATTEMPTS = 2;
|
|
14
|
+
const DEFAULT_THREAD_START_BACKOFF_MS = 5_000;
|
|
15
|
+
const MIN_THREAD_START_TIMEOUT_MS = 60_000;
|
|
16
|
+
const MAX_THREAD_START_TIMEOUT_MS = 180_000;
|
|
17
|
+
const THREAD_START_PROMPT_CHARS_PER_STEP = 50_000;
|
|
18
|
+
const THREAD_START_TIMEOUT_STEP_MS = 30_000;
|
|
19
|
+
|
|
20
|
+
const CODEX_APP_CAPABILITIES = {
|
|
21
|
+
kind: "codex-app",
|
|
22
|
+
runtime: "app-server",
|
|
23
|
+
streaming: true,
|
|
24
|
+
structured_output: true,
|
|
25
|
+
// codex-app emits the started thread id, surfaced as provider_session_id.
|
|
26
|
+
// With options.sessionKeepAlive the app-server subprocess + thread stay
|
|
27
|
+
// live in codexSessions, so a follow-up run can resume the thread via
|
|
28
|
+
// options.sessionId. The protocol still has no thread/load primitive, so
|
|
29
|
+
// resume only works while the subprocess is alive.
|
|
30
|
+
supports_session_resume: true,
|
|
31
|
+
native_runtime_config: null,
|
|
32
|
+
supports_mcp: true,
|
|
33
|
+
supports_skills: true,
|
|
34
|
+
supports_builtin_tools: true,
|
|
35
|
+
supports_live_input: true,
|
|
36
|
+
supports_native_subagents: true,
|
|
37
|
+
supports_fast_mode: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function promptFromMessages(messages) {
|
|
41
|
+
return Array.isArray(messages)
|
|
42
|
+
? messages.map((message) => typeof message.content === "string" ? message.content : JSON.stringify(message.content)).join("\n\n")
|
|
43
|
+
: String(messages || "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pushUniqueText(texts, text) {
|
|
47
|
+
const value = typeof text === "string" ? text.trim() : "";
|
|
48
|
+
if (!value) return;
|
|
49
|
+
if (texts.some((existing) => existing.trim() === value)) return;
|
|
50
|
+
texts.push(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function userTextInput(text) {
|
|
54
|
+
return [{ type: "text", text: String(text || ""), text_elements: [] }];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function integerOption(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
58
|
+
const n = Number(value);
|
|
59
|
+
if (!Number.isFinite(n)) return fallback;
|
|
60
|
+
return Math.min(max, Math.max(min, Math.trunc(n)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clamp(value, min, max) {
|
|
64
|
+
return Math.min(max, Math.max(min, value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultThreadStartTimeoutMs(systemPrompt) {
|
|
68
|
+
const promptChars = String(systemPrompt || "").length;
|
|
69
|
+
const sizeSteps = Math.max(0, Math.ceil(promptChars / THREAD_START_PROMPT_CHARS_PER_STEP) - 1);
|
|
70
|
+
return clamp(
|
|
71
|
+
MIN_THREAD_START_TIMEOUT_MS + (sizeSteps * THREAD_START_TIMEOUT_STEP_MS),
|
|
72
|
+
MIN_THREAD_START_TIMEOUT_MS,
|
|
73
|
+
MAX_THREAD_START_TIMEOUT_MS,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function threadStartPolicy(systemPrompt, options = {}) {
|
|
78
|
+
return {
|
|
79
|
+
timeoutMs: integerOption(
|
|
80
|
+
options.codexThreadStartTimeoutMs,
|
|
81
|
+
defaultThreadStartTimeoutMs(systemPrompt),
|
|
82
|
+
{ min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
83
|
+
),
|
|
84
|
+
attempts: integerOption(
|
|
85
|
+
options.codexThreadStartAttempts,
|
|
86
|
+
DEFAULT_THREAD_START_ATTEMPTS,
|
|
87
|
+
{ min: 1, max: 5 },
|
|
88
|
+
),
|
|
89
|
+
backoffMs: integerOption(
|
|
90
|
+
options.codexThreadStartBackoffMs,
|
|
91
|
+
DEFAULT_THREAD_START_BACKOFF_MS,
|
|
92
|
+
{ min: 0, max: 300_000 },
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function delay(ms, signal) {
|
|
98
|
+
const timeoutMs = Math.max(0, Number(ms) || 0);
|
|
99
|
+
if (!timeoutMs || signal?.aborted) return Promise.resolve();
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
102
|
+
timer.unref?.();
|
|
103
|
+
signal?.addEventListener?.("abort", () => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
resolve();
|
|
106
|
+
}, { once: true });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sandboxForPermissionMode(permissionMode) {
|
|
111
|
+
if (permissionMode === "bypassPermissions") return "danger-full-access";
|
|
112
|
+
if (permissionMode === "plan") return "read-only";
|
|
113
|
+
return "workspace-write";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function approvalPolicyForPermissionMode(permissionMode) {
|
|
117
|
+
if (permissionMode === "bypassPermissions") return "never";
|
|
118
|
+
if (permissionMode === "plan") return "on-request";
|
|
119
|
+
return "on-failure";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function codexMcpConfig(mcpServers = {}) {
|
|
123
|
+
const servers = {};
|
|
124
|
+
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
125
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) continue;
|
|
126
|
+
if (cfg?.command) {
|
|
127
|
+
servers[name] = {
|
|
128
|
+
command: cfg.command,
|
|
129
|
+
...(Array.isArray(cfg.args) ? { args: cfg.args } : {}),
|
|
130
|
+
...(cfg.env && typeof cfg.env === "object" ? { env: cfg.env } : {}),
|
|
131
|
+
...(cfg.cwd && typeof cfg.cwd === "string" ? { cwd: cfg.cwd } : {}),
|
|
132
|
+
enabled: true,
|
|
133
|
+
required: false,
|
|
134
|
+
};
|
|
135
|
+
} else if (cfg?.url) {
|
|
136
|
+
servers[name] = {
|
|
137
|
+
url: cfg.url,
|
|
138
|
+
...(cfg.headers && typeof cfg.headers === "object" ? { http_headers: cfg.headers } : {}),
|
|
139
|
+
enabled: true,
|
|
140
|
+
required: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return servers;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function codexErrorMessage(error) {
|
|
148
|
+
if (!error) return "Codex app-server error";
|
|
149
|
+
if (typeof error === "string") return error;
|
|
150
|
+
const data = error.data || error.error || {};
|
|
151
|
+
const info = data.info || data.code || error.code;
|
|
152
|
+
if (info && typeof info === "object" && "activeTurnNotSteerable" in info) {
|
|
153
|
+
return "Codex active turn is not steerable";
|
|
154
|
+
}
|
|
155
|
+
return error.message || data.message || JSON.stringify(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isActiveTurnNotSteerable(error) {
|
|
159
|
+
const info = error?.data?.info || error?.data?.error?.info || error?.info;
|
|
160
|
+
return info === "activeTurnNotSteerable" || Boolean(info?.activeTurnNotSteerable);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isNoActiveTurnToSteer(error) {
|
|
164
|
+
return /no active turn to steer/i.test(codexErrorMessage(error));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isCodexRequestTimeout(error, method = null) {
|
|
168
|
+
return error?.code === "CODEX_APP_SERVER_REQUEST_TIMEOUT"
|
|
169
|
+
&& (!method || error.method === method);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function codexErrorDiagnostics(error) {
|
|
173
|
+
if (!error) return {};
|
|
174
|
+
if (isCodexRequestTimeout(error)) {
|
|
175
|
+
return {
|
|
176
|
+
codex_error_code: "codex_app_server_request_timeout",
|
|
177
|
+
codex_request_method: error.method || null,
|
|
178
|
+
codex_request_timeout_ms: error.timeoutMs || null,
|
|
179
|
+
...(error.stderrTail ? { stderr_tail: error.stderrTail } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return error.code ? { codex_error_code: String(error.code) } : {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function withoutCodexRequestErrorDiagnostics(diagnostics) {
|
|
186
|
+
const {
|
|
187
|
+
codex_error_code: _codexErrorCode,
|
|
188
|
+
codex_request_method: _codexRequestMethod,
|
|
189
|
+
codex_request_timeout_ms: _codexRequestTimeoutMs,
|
|
190
|
+
stderr_tail: _stderrTail,
|
|
191
|
+
...rest
|
|
192
|
+
} = diagnostics || {};
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function codexNativeTeammates(nativeSubagents) {
|
|
197
|
+
if (nativeSubagents?.provider !== "codex" || !Array.isArray(nativeSubagents.teammates)) return [];
|
|
198
|
+
return nativeSubagents.teammates.map((agent) => {
|
|
199
|
+
const name = String(agent?.name || "").trim();
|
|
200
|
+
if (!name) return null;
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
displayName: agent.displayName || name,
|
|
204
|
+
description: agent.description || "",
|
|
205
|
+
model: agent.model?.model || agent.modelRef || null,
|
|
206
|
+
reasoningEffort: agent.effort || null,
|
|
207
|
+
instructions: agent.helperSystemPrompt || agent.instructions || "",
|
|
208
|
+
};
|
|
209
|
+
}).filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function codexCollaborationModePayload(nativeSubagents, { model, effort, systemPrompt }) {
|
|
213
|
+
const teammates = codexNativeTeammates(nativeSubagents);
|
|
214
|
+
if (!teammates.length) return null;
|
|
215
|
+
return {
|
|
216
|
+
mode: "default",
|
|
217
|
+
teammates,
|
|
218
|
+
settings: {
|
|
219
|
+
model,
|
|
220
|
+
reasoningEffort: effort || null,
|
|
221
|
+
developerInstructions: systemPrompt,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createCodexAppServerClient({
|
|
227
|
+
command = "codex",
|
|
228
|
+
// project_doc_max_bytes=0 keeps codex from injecting its own project docs;
|
|
229
|
+
// the host supplies the full context through developerInstructions.
|
|
230
|
+
args = ["app-server", "--listen", "stdio://", "-c", "project_doc_max_bytes=0"],
|
|
231
|
+
cwd,
|
|
232
|
+
env = {},
|
|
233
|
+
onNotification = () => {},
|
|
234
|
+
} = {}) {
|
|
235
|
+
const child = spawn(command, args, {
|
|
236
|
+
cwd,
|
|
237
|
+
env: { ...process.env, ...env },
|
|
238
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
239
|
+
});
|
|
240
|
+
const pending = new Map();
|
|
241
|
+
const stderr = [];
|
|
242
|
+
let nextId = 1;
|
|
243
|
+
let closed = false;
|
|
244
|
+
let resolveClosed;
|
|
245
|
+
const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
|
|
246
|
+
|
|
247
|
+
function rejectAll(err) {
|
|
248
|
+
for (const { reject, timer } of pending.values()) {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
reject(err);
|
|
251
|
+
}
|
|
252
|
+
pending.clear();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function stderrTail() {
|
|
256
|
+
const text = stderr.join("").trim();
|
|
257
|
+
if (!text) return "";
|
|
258
|
+
return text.length > 8_192 ? text.slice(text.length - 8_192) : text;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk.toString()));
|
|
262
|
+
|
|
263
|
+
const rl = createInterface({ input: child.stdout });
|
|
264
|
+
rl.on("line", (line) => {
|
|
265
|
+
if (!line.trim()) return;
|
|
266
|
+
let message;
|
|
267
|
+
try {
|
|
268
|
+
message = JSON.parse(line);
|
|
269
|
+
} catch {
|
|
270
|
+
onNotification({ method: "warning", params: { message: `Malformed Codex app-server output: ${line}` } });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (Object.prototype.hasOwnProperty.call(message, "id") && (message.result !== undefined || message.error !== undefined)) {
|
|
274
|
+
const entry = pending.get(message.id);
|
|
275
|
+
if (!entry) return;
|
|
276
|
+
pending.delete(message.id);
|
|
277
|
+
clearTimeout(entry.timer);
|
|
278
|
+
if (message.error) entry.reject(Object.assign(new Error(codexErrorMessage(message.error)), { responseError: message.error }));
|
|
279
|
+
else entry.resolve(message.result);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (message.method) onNotification(message);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
child.on("error", (err) => {
|
|
286
|
+
closed = true;
|
|
287
|
+
rejectAll(err);
|
|
288
|
+
resolveClosed(err);
|
|
289
|
+
});
|
|
290
|
+
child.on("close", (code) => {
|
|
291
|
+
closed = true;
|
|
292
|
+
const detail = stderr.join("").trim();
|
|
293
|
+
const err = new Error(detail || `codex app-server exited ${code}`);
|
|
294
|
+
rejectAll(err);
|
|
295
|
+
resolveClosed(err);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
function request(method, params, { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
299
|
+
if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) {
|
|
300
|
+
return Promise.reject(new Error("codex app-server is not running"));
|
|
301
|
+
}
|
|
302
|
+
const id = nextId++;
|
|
303
|
+
const payload = { id, method, params };
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
const timer = setTimeout(() => {
|
|
306
|
+
pending.delete(id);
|
|
307
|
+
reject(Object.assign(new Error(`codex app-server request timed out: ${method}`), {
|
|
308
|
+
code: "CODEX_APP_SERVER_REQUEST_TIMEOUT",
|
|
309
|
+
method,
|
|
310
|
+
timeoutMs,
|
|
311
|
+
stderrTail: stderrTail(),
|
|
312
|
+
}));
|
|
313
|
+
}, timeoutMs);
|
|
314
|
+
timer.unref?.();
|
|
315
|
+
pending.set(id, { resolve, reject, timer });
|
|
316
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
|
|
317
|
+
if (!err) return;
|
|
318
|
+
pending.delete(id);
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
reject(err);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function close() {
|
|
326
|
+
if (closed) return;
|
|
327
|
+
closed = true;
|
|
328
|
+
try { child.stdin?.end?.(); } catch {}
|
|
329
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
330
|
+
rejectAll(new Error("codex app-server closed"));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { request, close, child, closed: closedPromise };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function mapThreadItem(method, item) {
|
|
337
|
+
if (!item || typeof item !== "object") return null;
|
|
338
|
+
const type = method.endsWith("started") ? "item.started" : "item.completed";
|
|
339
|
+
if (item.type === "agentMessage") {
|
|
340
|
+
return { type, item: { type: "agent_message", id: item.id, text: item.text || "" } };
|
|
341
|
+
}
|
|
342
|
+
if (item.type === "commandExecution") {
|
|
343
|
+
return {
|
|
344
|
+
type,
|
|
345
|
+
item: {
|
|
346
|
+
type: "command_execution",
|
|
347
|
+
id: item.id,
|
|
348
|
+
command: item.command,
|
|
349
|
+
aggregated_output: item.aggregatedOutput || "",
|
|
350
|
+
exit_code: item.exitCode,
|
|
351
|
+
status: item.status,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (item.type === "fileChange") {
|
|
356
|
+
return {
|
|
357
|
+
type,
|
|
358
|
+
item: {
|
|
359
|
+
type: "file_change",
|
|
360
|
+
id: item.id,
|
|
361
|
+
changes: item.changes || [],
|
|
362
|
+
status: item.status,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (item.type === "mcpToolCall") {
|
|
367
|
+
return {
|
|
368
|
+
type,
|
|
369
|
+
item: {
|
|
370
|
+
type: "mcp_tool_call",
|
|
371
|
+
id: item.id,
|
|
372
|
+
server: item.server,
|
|
373
|
+
tool: item.tool,
|
|
374
|
+
arguments: item.arguments,
|
|
375
|
+
result: item.result,
|
|
376
|
+
error: item.error,
|
|
377
|
+
status: item.status,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (item.type === "collabAgentToolCall") {
|
|
382
|
+
const name = `codex_${item.tool || "subagent"}`;
|
|
383
|
+
if (method.endsWith("started")) {
|
|
384
|
+
return {
|
|
385
|
+
type: "assistant",
|
|
386
|
+
message: {
|
|
387
|
+
content: [{
|
|
388
|
+
type: "tool_use",
|
|
389
|
+
id: item.id,
|
|
390
|
+
name,
|
|
391
|
+
input: {
|
|
392
|
+
prompt: item.prompt,
|
|
393
|
+
model: item.model,
|
|
394
|
+
reasoningEffort: item.reasoningEffort,
|
|
395
|
+
receiverThreadIds: item.receiverThreadIds || [],
|
|
396
|
+
},
|
|
397
|
+
}],
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
type: "user",
|
|
403
|
+
message: {
|
|
404
|
+
content: [{
|
|
405
|
+
type: "tool_result",
|
|
406
|
+
tool_use_id: item.id,
|
|
407
|
+
content: {
|
|
408
|
+
status: item.status,
|
|
409
|
+
receiverThreadIds: item.receiverThreadIds || [],
|
|
410
|
+
agentsStates: item.agentsStates || [],
|
|
411
|
+
...(item.error ? { error: item.error } : {}),
|
|
412
|
+
},
|
|
413
|
+
is_error: item.status === "failed" || Boolean(item.error),
|
|
414
|
+
}],
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (item.type === "reasoning") {
|
|
419
|
+
const text = [...(item.summary || []), ...(item.content || [])].join("\n").trim();
|
|
420
|
+
return text ? { type: "assistant", message: { content: [{ type: "thinking", text }] } } : null;
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function usageFromTokenUsage(tokenUsage) {
|
|
426
|
+
const last = tokenUsage?.last || tokenUsage?.total || {};
|
|
427
|
+
return {
|
|
428
|
+
input_tokens: last.inputTokens ?? null,
|
|
429
|
+
output_tokens: last.outputTokens ?? null,
|
|
430
|
+
cache_read_tokens: last.cachedInputTokens ?? null,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const noopNotificationHandler = () => {};
|
|
435
|
+
|
|
436
|
+
// Live keep-alive sessions keyed by codex thread id.
|
|
437
|
+
const codexSessions = createSessionRegistry({
|
|
438
|
+
isBusy: (entry) => entry.busy === true,
|
|
439
|
+
onEvict: async (entry) => {
|
|
440
|
+
try { entry.client.close(); } catch {}
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
445
|
+
const start = Date.now();
|
|
446
|
+
const resolved = options.model;
|
|
447
|
+
// Test seam: lets tests drive the bridge with a stub app-server client.
|
|
448
|
+
const makeClient = options.codexClientFactory || createCodexAppServerClient;
|
|
449
|
+
const keepAlive = options.sessionKeepAlive === true;
|
|
450
|
+
// The bridge TTL is a backstop behind the host's session policy; the grace
|
|
451
|
+
// keeps the host's lazy expiry firing first so eviction stays host-driven.
|
|
452
|
+
const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
|
|
453
|
+
? Number(options.sessionIdleTimeoutMs) + 60_000
|
|
454
|
+
: undefined;
|
|
455
|
+
const resumeSessionId = typeof options.sessionId === "string" && options.sessionId.trim()
|
|
456
|
+
? options.sessionId
|
|
457
|
+
: null;
|
|
458
|
+
const prompt = promptFromMessages(options.messages);
|
|
459
|
+
// Effort is expected to be pre-normalized by core/ai.js#generateResponse
|
|
460
|
+
// before reaching this provider. We trust options.effort verbatim.
|
|
461
|
+
const normalizedEffort = typeof options.effort === "string" && options.effort.trim()
|
|
462
|
+
? options.effort
|
|
463
|
+
: null;
|
|
464
|
+
const events = [];
|
|
465
|
+
const texts = [];
|
|
466
|
+
const agentTextByItem = new Map();
|
|
467
|
+
let threadId = null;
|
|
468
|
+
let activeTurnId = null;
|
|
469
|
+
let turnCompleted = false;
|
|
470
|
+
let errorMessage = null;
|
|
471
|
+
let failureKind = null;
|
|
472
|
+
let usage = {};
|
|
473
|
+
let codexDiagnostics = {};
|
|
474
|
+
let resolveTurn;
|
|
475
|
+
let resolveTurnReady;
|
|
476
|
+
let turnReadyResolved = false;
|
|
477
|
+
const fileChangeSnapshots = new Map();
|
|
478
|
+
const codexItemContext = {
|
|
479
|
+
fileChangePayload: (raw) => createFileChangePayload(raw, {
|
|
480
|
+
cwd: options.cwd || process.cwd(),
|
|
481
|
+
snapshots: fileChangeSnapshots,
|
|
482
|
+
}),
|
|
483
|
+
};
|
|
484
|
+
const turnDone = new Promise((resolve) => { resolveTurn = resolve; });
|
|
485
|
+
const turnReady = new Promise((resolve) => { resolveTurnReady = resolve; });
|
|
486
|
+
|
|
487
|
+
function setActiveTurnId(turnId, { steerReady = false } = {}) {
|
|
488
|
+
activeTurnId = turnId || activeTurnId;
|
|
489
|
+
if (steerReady && !turnReadyResolved && threadId && activeTurnId) {
|
|
490
|
+
turnReadyResolved = true;
|
|
491
|
+
resolveTurnReady();
|
|
492
|
+
}
|
|
493
|
+
// An abort that fired before the turn id was known could not interrupt;
|
|
494
|
+
// deliver it as soon as the turn becomes addressable.
|
|
495
|
+
if (abortRequested && !interruptSent && threadId && activeTurnId) {
|
|
496
|
+
interruptSent = true;
|
|
497
|
+
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function emitEvent(event) {
|
|
502
|
+
if (!event) return;
|
|
503
|
+
events.push(event);
|
|
504
|
+
options.onEvent?.(event);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function handleAgentText(text) {
|
|
508
|
+
pushUniqueText(texts, text);
|
|
509
|
+
emitEvent({ type: "assistant", message: { content: [{ type: "text", text }] } });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function handleNotification(notification) {
|
|
513
|
+
const { method, params = {} } = notification;
|
|
514
|
+
if (method === "turn/started") {
|
|
515
|
+
setActiveTurnId(params.turn?.id, { steerReady: true });
|
|
516
|
+
emitEvent({ type: "cli_event", raw: { type: "turn_started", turn: params.turn } });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (method === "turn/completed") {
|
|
520
|
+
setActiveTurnId(params.turn?.id);
|
|
521
|
+
turnCompleted = true;
|
|
522
|
+
if (params.turn?.status === "failed") {
|
|
523
|
+
errorMessage = params.turn?.error?.message || params.turn?.error || "Codex turn failed";
|
|
524
|
+
failureKind = "provider_unavailable";
|
|
525
|
+
}
|
|
526
|
+
emitEvent({ type: "cli_event", raw: { type: "turn_completed", turn: params.turn } });
|
|
527
|
+
resolveTurn(params.turn);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (method === "thread/tokenUsage/updated") {
|
|
531
|
+
usage = usageFromTokenUsage(params.tokenUsage);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (method === "item/agentMessage/delta") {
|
|
535
|
+
const current = agentTextByItem.get(params.itemId) || "";
|
|
536
|
+
agentTextByItem.set(params.itemId, `${current}${params.delta || ""}`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
540
|
+
emitEvent({ type: "assistant", message: { content: [{ type: "thinking", text: params.delta || "" }] } });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (method === "warning" || method === "error" || method === "configWarning" || method === "guardianWarning") {
|
|
544
|
+
emitEvent({
|
|
545
|
+
type: "runtime_warning",
|
|
546
|
+
warning_kind: method.replace(/\W+/g, "_"),
|
|
547
|
+
message: params.message || params.error || JSON.stringify(params),
|
|
548
|
+
});
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (method === "item/started" || method === "item/completed") {
|
|
552
|
+
const raw = mapThreadItem(method, params.item);
|
|
553
|
+
if (params.item?.type === "agentMessage") {
|
|
554
|
+
const text = params.item.text || agentTextByItem.get(params.item.id) || "";
|
|
555
|
+
if (method === "item/completed") handleAgentText(text);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (raw) emitEvent(normalizeCodexItemEvent(raw, codexItemContext) || raw);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let client = null;
|
|
563
|
+
let resumeEntry = null;
|
|
564
|
+
let sessionRetained = false;
|
|
565
|
+
let abortRequested = false;
|
|
566
|
+
let interruptSent = false;
|
|
567
|
+
// Mutable holder so keep-alive clients can outlive this run: each run
|
|
568
|
+
// installs its own handleNotification and the bridge restores a no-op
|
|
569
|
+
// once the session goes idle.
|
|
570
|
+
const notificationTarget = { handler: handleNotification };
|
|
571
|
+
function createClient() {
|
|
572
|
+
return makeClient({
|
|
573
|
+
command: options.codexAppServerCommand,
|
|
574
|
+
args: options.codexAppServerArgs,
|
|
575
|
+
cwd: options.cwd,
|
|
576
|
+
env: options.codexAppServerEnv,
|
|
577
|
+
onNotification: (notification) => notificationTarget.handler(notification),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function initializeClient(nextClient) {
|
|
582
|
+
const brand = readRuntimeBrand();
|
|
583
|
+
await nextClient.request("initialize", {
|
|
584
|
+
clientInfo: { name: brand.clientInfoName, title: brand.clientInfoTitle, version: "0" },
|
|
585
|
+
capabilities: { experimentalApi: true },
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function requestThreadStart(params) {
|
|
590
|
+
const policy = threadStartPolicy(systemPrompt, options);
|
|
591
|
+
const startedAt = Date.now();
|
|
592
|
+
let lastError = null;
|
|
593
|
+
for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
|
|
594
|
+
if (!client) {
|
|
595
|
+
client = createClient();
|
|
596
|
+
await initializeClient(client);
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const thread = await client.request("thread/start", params, { timeoutMs: policy.timeoutMs });
|
|
600
|
+
codexDiagnostics = {
|
|
601
|
+
...withoutCodexRequestErrorDiagnostics(codexDiagnostics),
|
|
602
|
+
codex_thread_start_attempts: attempt,
|
|
603
|
+
codex_thread_start_timeout_ms: policy.timeoutMs,
|
|
604
|
+
codex_thread_start_duration_ms: Date.now() - startedAt,
|
|
605
|
+
...(attempt > 1 ? { codex_thread_start_retried: true } : {}),
|
|
606
|
+
};
|
|
607
|
+
return thread;
|
|
608
|
+
} catch (err) {
|
|
609
|
+
lastError = err;
|
|
610
|
+
codexDiagnostics = {
|
|
611
|
+
...codexDiagnostics,
|
|
612
|
+
...codexErrorDiagnostics(err),
|
|
613
|
+
codex_thread_start_attempts: attempt,
|
|
614
|
+
codex_thread_start_timeout_ms: policy.timeoutMs,
|
|
615
|
+
codex_thread_start_duration_ms: Date.now() - startedAt,
|
|
616
|
+
...(attempt > 1 ? { codex_thread_start_retried: true } : {}),
|
|
617
|
+
};
|
|
618
|
+
if (!isCodexRequestTimeout(err, "thread/start") || attempt >= policy.attempts || options.abortSignal?.aborted) {
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
emitEvent({
|
|
622
|
+
type: "runtime_warning",
|
|
623
|
+
warning_kind: "codex_thread_start_retry",
|
|
624
|
+
message: `Codex app-server thread/start timed out after ${policy.timeoutMs}ms; retrying with a fresh app-server.`,
|
|
625
|
+
});
|
|
626
|
+
client.close();
|
|
627
|
+
client = null;
|
|
628
|
+
await delay(policy.backoffMs, options.abortSignal);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
throw lastError || new Error("codex app-server request timed out: thread/start");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const abortHandler = () => {
|
|
635
|
+
abortRequested = true;
|
|
636
|
+
if (threadId && activeTurnId && !interruptSent) {
|
|
637
|
+
interruptSent = true;
|
|
638
|
+
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
639
|
+
}
|
|
640
|
+
// Resumed sessions stay alive across an interrupt; only fresh runs tear
|
|
641
|
+
// down their subprocess on abort.
|
|
642
|
+
if (!resumeEntry) client?.close();
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
async function steerLiveInput() {
|
|
646
|
+
if (!options.liveInput) return;
|
|
647
|
+
for await (const message of options.liveInput) {
|
|
648
|
+
if (turnCompleted) break;
|
|
649
|
+
if (!threadId || !activeTurnId || !turnReadyResolved) {
|
|
650
|
+
await Promise.race([
|
|
651
|
+
turnReady,
|
|
652
|
+
turnDone,
|
|
653
|
+
client.closed.then((err) => { throw err; }),
|
|
654
|
+
]);
|
|
655
|
+
if (turnCompleted || !turnReadyResolved) break;
|
|
656
|
+
}
|
|
657
|
+
const input = userTextInput(formatLiveInputGuidance(message.body));
|
|
658
|
+
try {
|
|
659
|
+
const response = await client.request("turn/steer", {
|
|
660
|
+
threadId,
|
|
661
|
+
expectedTurnId: activeTurnId,
|
|
662
|
+
input,
|
|
663
|
+
});
|
|
664
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
665
|
+
} catch (err) {
|
|
666
|
+
const providerError = err?.responseError;
|
|
667
|
+
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
668
|
+
await Promise.race([
|
|
669
|
+
turnReady,
|
|
670
|
+
turnDone,
|
|
671
|
+
client.closed.then((closedErr) => { throw closedErr; }),
|
|
672
|
+
]);
|
|
673
|
+
if (turnCompleted) break;
|
|
674
|
+
try {
|
|
675
|
+
const response = await client.request("turn/steer", {
|
|
676
|
+
threadId,
|
|
677
|
+
expectedTurnId: activeTurnId,
|
|
678
|
+
input,
|
|
679
|
+
});
|
|
680
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
681
|
+
continue;
|
|
682
|
+
} catch (retryErr) {
|
|
683
|
+
const retryProviderError = retryErr?.responseError;
|
|
684
|
+
emitEvent({
|
|
685
|
+
type: "runtime_warning",
|
|
686
|
+
warning_kind: isActiveTurnNotSteerable(retryProviderError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
687
|
+
message: codexErrorMessage(retryProviderError || retryErr),
|
|
688
|
+
});
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
emitEvent({
|
|
693
|
+
type: "runtime_warning",
|
|
694
|
+
warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
695
|
+
message: codexErrorMessage(providerError || err),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function sessionUnavailableResult(kind, error, codexErrorCode) {
|
|
702
|
+
return {
|
|
703
|
+
text: null,
|
|
704
|
+
structuredResult: undefined,
|
|
705
|
+
structuredResultSource: null,
|
|
706
|
+
events: [],
|
|
707
|
+
usage: {},
|
|
708
|
+
durationMs: Date.now() - start,
|
|
709
|
+
numTurns: 0,
|
|
710
|
+
model: resolved?.reference || `codex:${resolved?.model || ""}`,
|
|
711
|
+
effort: options.effort || null,
|
|
712
|
+
sdk: "codex",
|
|
713
|
+
providerSessionId: resumeSessionId,
|
|
714
|
+
provider_session_id: resumeSessionId,
|
|
715
|
+
cancelled: false,
|
|
716
|
+
error,
|
|
717
|
+
failureKind: kind,
|
|
718
|
+
diagnostics: { codex_error_code: codexErrorCode },
|
|
719
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
720
|
+
promptCacheActive: null,
|
|
721
|
+
thinkingEnabled: null,
|
|
722
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
723
|
+
subagentInvoked: null,
|
|
724
|
+
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
725
|
+
nativeSubagentsUsed: [],
|
|
726
|
+
toolCompactionApplied: false,
|
|
727
|
+
contextCompactionApplied: null,
|
|
728
|
+
}),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (resumeSessionId) {
|
|
733
|
+
const entry = codexSessions.get(resumeSessionId);
|
|
734
|
+
if (!entry) {
|
|
735
|
+
// The host sent no conversation history for a resume, so silently
|
|
736
|
+
// starting a fresh thread would lose context. Fail fast instead.
|
|
737
|
+
return sessionUnavailableResult(
|
|
738
|
+
"session_not_found",
|
|
739
|
+
`Codex session ${resumeSessionId} is not live; cannot resume`,
|
|
740
|
+
"codex_session_not_found",
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (entry.busy) {
|
|
744
|
+
return sessionUnavailableResult(
|
|
745
|
+
"session_busy",
|
|
746
|
+
`Codex session ${resumeSessionId} is already executing a turn`,
|
|
747
|
+
"codex_session_busy",
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
entry.busy = true;
|
|
751
|
+
resumeEntry = entry;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
if (resumeEntry) {
|
|
756
|
+
client = resumeEntry.client;
|
|
757
|
+
threadId = resumeEntry.threadId;
|
|
758
|
+
resumeEntry.notificationTarget.handler = handleNotification;
|
|
759
|
+
// Keep the idle TTL from firing while the turn is in flight.
|
|
760
|
+
codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
761
|
+
} else {
|
|
762
|
+
client = createClient();
|
|
763
|
+
}
|
|
764
|
+
if (options.abortSignal) {
|
|
765
|
+
if (options.abortSignal.aborted) abortHandler();
|
|
766
|
+
else options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
767
|
+
}
|
|
768
|
+
if (!resumeEntry) await initializeClient(client);
|
|
769
|
+
let collaborationMode = codexCollaborationModePayload(options.nativeSubagents, {
|
|
770
|
+
model: resolved.model,
|
|
771
|
+
effort: normalizedEffort,
|
|
772
|
+
systemPrompt,
|
|
773
|
+
});
|
|
774
|
+
if (collaborationMode) {
|
|
775
|
+
try {
|
|
776
|
+
await client.request("collaborationMode/list", {}, { timeoutMs: 5_000 });
|
|
777
|
+
} catch (err) {
|
|
778
|
+
emitEvent({
|
|
779
|
+
type: "runtime_warning",
|
|
780
|
+
warning_kind: "codex_collaboration_mode_unavailable",
|
|
781
|
+
message: codexErrorMessage(err?.responseError || err),
|
|
782
|
+
});
|
|
783
|
+
collaborationMode = null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const fastMode = codexModelSupportsFastMode(resolved.model) && normalizeFastMode(options.fastMode, true);
|
|
787
|
+
if (!resumeEntry) {
|
|
788
|
+
const mcpServers = codexMcpConfig(options.mcpServers);
|
|
789
|
+
const config = {
|
|
790
|
+
...(fastMode ? { service_tier: "fast" } : {}),
|
|
791
|
+
features: { fast_mode: fastMode },
|
|
792
|
+
...(Object.keys(mcpServers).length ? { mcp_servers: mcpServers } : {}),
|
|
793
|
+
};
|
|
794
|
+
if (normalizedEffort) {
|
|
795
|
+
config.model_reasoning_effort = normalizedEffort;
|
|
796
|
+
if (normalizedEffort !== "none") config.model_reasoning_summary = "auto";
|
|
797
|
+
}
|
|
798
|
+
// The codex app-server protocol exposes thread/start but no thread/load
|
|
799
|
+
// primitive, so cold continuations always start a fresh thread; a
|
|
800
|
+
// thread is only resumable while its subprocess stays live in
|
|
801
|
+
// codexSessions (options.sessionKeepAlive + options.sessionId).
|
|
802
|
+
const thread = await requestThreadStart({
|
|
803
|
+
model: resolved.model,
|
|
804
|
+
modelProvider: "openai",
|
|
805
|
+
...(fastMode ? { serviceTier: "fast" } : {}),
|
|
806
|
+
cwd: options.cwd || process.cwd(),
|
|
807
|
+
approvalPolicy: approvalPolicyForPermissionMode(options.permissionMode),
|
|
808
|
+
sandbox: sandboxForPermissionMode(options.permissionMode),
|
|
809
|
+
config,
|
|
810
|
+
serviceName: readRuntimeBrand().serviceName,
|
|
811
|
+
developerInstructions: systemPrompt,
|
|
812
|
+
ephemeral: true,
|
|
813
|
+
sessionStartSource: "startup",
|
|
814
|
+
experimentalRawEvents: false,
|
|
815
|
+
persistExtendedHistory: false,
|
|
816
|
+
});
|
|
817
|
+
threadId = thread?.thread?.id;
|
|
818
|
+
if (!threadId) throw new Error("Codex app-server did not return a thread id");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const steerTask = steerLiveInput();
|
|
822
|
+
steerTask.catch((err) => {
|
|
823
|
+
emitEvent({
|
|
824
|
+
type: "runtime_warning",
|
|
825
|
+
warning_kind: "live_input_failed",
|
|
826
|
+
message: err?.message || String(err),
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
const turnParams = {
|
|
830
|
+
threadId,
|
|
831
|
+
input: userTextInput(prompt),
|
|
832
|
+
cwd: options.cwd || process.cwd(),
|
|
833
|
+
approvalPolicy: approvalPolicyForPermissionMode(options.permissionMode),
|
|
834
|
+
sandboxPolicy: options.permissionMode === "bypassPermissions" ? { type: "dangerFullAccess" } : null,
|
|
835
|
+
model: resolved.model,
|
|
836
|
+
...(fastMode ? { serviceTier: "fast" } : {}),
|
|
837
|
+
effort: normalizedEffort,
|
|
838
|
+
summary: normalizedEffort && normalizedEffort !== "none" ? "auto" : "none",
|
|
839
|
+
outputSchema: options.outputSchema,
|
|
840
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
841
|
+
};
|
|
842
|
+
let turn;
|
|
843
|
+
try {
|
|
844
|
+
turn = await client.request("turn/start", turnParams);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (!collaborationMode) throw err;
|
|
847
|
+
emitEvent({
|
|
848
|
+
type: "runtime_warning",
|
|
849
|
+
warning_kind: "codex_collaboration_mode_rejected",
|
|
850
|
+
message: codexErrorMessage(err?.responseError || err),
|
|
851
|
+
});
|
|
852
|
+
const fallbackParams = { ...turnParams };
|
|
853
|
+
delete fallbackParams.collaborationMode;
|
|
854
|
+
turn = await client.request("turn/start", fallbackParams);
|
|
855
|
+
}
|
|
856
|
+
setActiveTurnId(turn?.turn?.id);
|
|
857
|
+
|
|
858
|
+
let prematureClose = false;
|
|
859
|
+
// Resumed runs watch subprocess death through the entry's mutable hook
|
|
860
|
+
// instead of client.closed.then: a long-lived thread would otherwise
|
|
861
|
+
// accumulate one permanent .then closure per turn.
|
|
862
|
+
const closedSignal = resumeEntry
|
|
863
|
+
? new Promise((resolve) => { resumeEntry.closedTarget.handler = resolve; })
|
|
864
|
+
: client.closed;
|
|
865
|
+
// Resumed runs never close the client on abort, so the wait must also
|
|
866
|
+
// resolve on the abort signal or an interrupted turn could hang forever.
|
|
867
|
+
let abortRaceCleanup = () => {};
|
|
868
|
+
const abortedSignal = resumeEntry && options.abortSignal
|
|
869
|
+
? new Promise((resolve) => {
|
|
870
|
+
if (options.abortSignal.aborted) {
|
|
871
|
+
resolve(null);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const onAbort = () => resolve(null);
|
|
875
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
876
|
+
abortRaceCleanup = () => options.abortSignal.removeEventListener?.("abort", onAbort);
|
|
877
|
+
})
|
|
878
|
+
: null;
|
|
879
|
+
try {
|
|
880
|
+
await Promise.race([
|
|
881
|
+
turnDone,
|
|
882
|
+
...(abortedSignal === null ? [] : [abortedSignal]),
|
|
883
|
+
closedSignal.then((err) => {
|
|
884
|
+
if (!turnCompleted) {
|
|
885
|
+
prematureClose = true;
|
|
886
|
+
throw err || new Error("codex app-server closed");
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}),
|
|
890
|
+
]);
|
|
891
|
+
} catch (err) {
|
|
892
|
+
if (prematureClose && !errorMessage) {
|
|
893
|
+
errorMessage = err?.message || "codex app-server stream closed before turn completed";
|
|
894
|
+
failureKind = "provider_unavailable";
|
|
895
|
+
} else if (!prematureClose) {
|
|
896
|
+
throw err;
|
|
897
|
+
}
|
|
898
|
+
} finally {
|
|
899
|
+
abortRaceCleanup();
|
|
900
|
+
}
|
|
901
|
+
turnCompleted = true;
|
|
902
|
+
await Promise.race([steerTask, Promise.resolve()]);
|
|
903
|
+
|
|
904
|
+
const text = texts[texts.length - 1] || "";
|
|
905
|
+
let codexErrorCode = prematureClose ? "codex_app_server_closed" : null;
|
|
906
|
+
if (!errorMessage && !text.trim()) {
|
|
907
|
+
errorMessage = "codex app-server completed without final output";
|
|
908
|
+
failureKind = "provider_unavailable";
|
|
909
|
+
codexErrorCode = codexErrorCode || "codex_app_server_no_output";
|
|
910
|
+
}
|
|
911
|
+
if (resumeEntry) {
|
|
912
|
+
// A failed turn or a closed transport leaves the thread untrustworthy,
|
|
913
|
+
// but an interrupt is normal steering: the aborted session survives.
|
|
914
|
+
const aborted = !!options.abortSignal?.aborted;
|
|
915
|
+
sessionRetained = (aborted && !prematureClose) || (!errorMessage && !failureKind);
|
|
916
|
+
if (sessionRetained) codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
917
|
+
else codexSessions.delete(resumeSessionId);
|
|
918
|
+
} else if (keepAlive && threadId && !errorMessage && !failureKind && !options.abortSignal?.aborted) {
|
|
919
|
+
sessionRetained = true;
|
|
920
|
+
notificationTarget.handler = noopNotificationHandler;
|
|
921
|
+
const entry = { client, threadId, busy: false, notificationTarget, closedTarget: { handler: null } };
|
|
922
|
+
codexSessions.set(threadId, entry, { idleTimeoutMs: sessionTtlMs });
|
|
923
|
+
client.closed.then(() => {
|
|
924
|
+
codexSessions.delete(threadId);
|
|
925
|
+
entry.closedTarget.handler?.(new Error("codex app-server closed"));
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const hadPartialProgress = events.length > 0 || texts.length > 0;
|
|
929
|
+
const reference = `codex:${resolved.model}`;
|
|
930
|
+
const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
|
|
931
|
+
const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
|
|
932
|
+
const cachedTokens = usage?.cache_read_tokens ?? usage?.cachedInputTokens ?? 0;
|
|
933
|
+
const cacheCreationTokens = usage?.cache_creation_tokens ?? usage?.cacheCreationTokens ?? 0;
|
|
934
|
+
const billableInputTokens = Math.max(0, inputTokens - cachedTokens - cacheCreationTokens);
|
|
935
|
+
const costUsd = estimateCost({
|
|
936
|
+
resolveCustomPricing: options.resolveCustomPricing,
|
|
937
|
+
model: reference,
|
|
938
|
+
inputTokens: billableInputTokens,
|
|
939
|
+
outputTokens,
|
|
940
|
+
cachedTokens,
|
|
941
|
+
cacheWriteTokens: cacheCreationTokens,
|
|
942
|
+
});
|
|
943
|
+
const enrichedUsage = {
|
|
944
|
+
...usage,
|
|
945
|
+
input_tokens: inputTokens || null,
|
|
946
|
+
output_tokens: outputTokens || null,
|
|
947
|
+
cache_read_tokens: cachedTokens || null,
|
|
948
|
+
cache_creation_tokens: cacheCreationTokens || null,
|
|
949
|
+
cost_usd: costUsd,
|
|
950
|
+
};
|
|
951
|
+
return {
|
|
952
|
+
text,
|
|
953
|
+
structuredResult: undefined,
|
|
954
|
+
structuredResultSource: null,
|
|
955
|
+
events,
|
|
956
|
+
usage: enrichedUsage,
|
|
957
|
+
durationMs: Date.now() - start,
|
|
958
|
+
numTurns: 1,
|
|
959
|
+
model: reference,
|
|
960
|
+
effort: options.effort || null,
|
|
961
|
+
sdk: "codex",
|
|
962
|
+
providerSessionId: threadId || null,
|
|
963
|
+
provider_session_id: threadId || null,
|
|
964
|
+
cancelled: !!options.abortSignal?.aborted,
|
|
965
|
+
error: errorMessage,
|
|
966
|
+
failureKind,
|
|
967
|
+
diagnostics: {
|
|
968
|
+
...codexDiagnostics,
|
|
969
|
+
...(codexErrorCode ? { codex_error_code: codexErrorCode } : {}),
|
|
970
|
+
...(hadPartialProgress && failureKind === "provider_unavailable"
|
|
971
|
+
? { had_partial_progress: true }
|
|
972
|
+
: {}),
|
|
973
|
+
},
|
|
974
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
975
|
+
promptCacheActive: (cachedTokens || 0) > 0 || (cacheCreationTokens || 0) > 0,
|
|
976
|
+
thinkingEnabled: null,
|
|
977
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
978
|
+
subagentInvoked: null,
|
|
979
|
+
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
980
|
+
nativeSubagentsUsed: [],
|
|
981
|
+
toolCompactionApplied: false,
|
|
982
|
+
contextCompactionApplied: null,
|
|
983
|
+
}),
|
|
984
|
+
};
|
|
985
|
+
} catch (err) {
|
|
986
|
+
if (resumeEntry) codexSessions.delete(resumeSessionId);
|
|
987
|
+
return {
|
|
988
|
+
text: texts[texts.length - 1] || null,
|
|
989
|
+
structuredResult: undefined,
|
|
990
|
+
structuredResultSource: null,
|
|
991
|
+
events,
|
|
992
|
+
usage,
|
|
993
|
+
durationMs: Date.now() - start,
|
|
994
|
+
numTurns: texts.length || (events.length ? 1 : 0),
|
|
995
|
+
model: resolved?.reference || `codex:${resolved?.model || ""}`,
|
|
996
|
+
effort: options.effort || null,
|
|
997
|
+
sdk: "codex",
|
|
998
|
+
providerSessionId: threadId || null,
|
|
999
|
+
provider_session_id: threadId || null,
|
|
1000
|
+
cancelled: !!options.abortSignal?.aborted,
|
|
1001
|
+
error: err?.message || String(err),
|
|
1002
|
+
failureKind: failureKind || "provider_unavailable",
|
|
1003
|
+
diagnostics: {
|
|
1004
|
+
...codexDiagnostics,
|
|
1005
|
+
...codexErrorDiagnostics(err),
|
|
1006
|
+
...(events.length > 0 || texts.length > 0 ? { had_partial_progress: true } : {}),
|
|
1007
|
+
},
|
|
1008
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
1009
|
+
promptCacheActive: null,
|
|
1010
|
+
thinkingEnabled: null,
|
|
1011
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
1012
|
+
subagentInvoked: null,
|
|
1013
|
+
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
1014
|
+
nativeSubagentsUsed: [],
|
|
1015
|
+
toolCompactionApplied: false,
|
|
1016
|
+
contextCompactionApplied: null,
|
|
1017
|
+
}),
|
|
1018
|
+
};
|
|
1019
|
+
} finally {
|
|
1020
|
+
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
1021
|
+
if (resumeEntry) {
|
|
1022
|
+
resumeEntry.busy = false;
|
|
1023
|
+
resumeEntry.notificationTarget.handler = noopNotificationHandler;
|
|
1024
|
+
resumeEntry.closedTarget.handler = null;
|
|
1025
|
+
}
|
|
1026
|
+
if (!sessionRetained) client?.close();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export const codexAppBackend = {
|
|
1031
|
+
kind: "codex-app",
|
|
1032
|
+
capabilities: CODEX_APP_CAPABILITIES,
|
|
1033
|
+
execute: generateCodexAppResponse,
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// CLI bridge for sdk='codex' agents that opt into execution_mode='cli'. The
|
|
1037
|
+
// codex `app-server` is more capable than `codex exec` (better event
|
|
1038
|
+
// streaming, MCP support), so this is the default CLI path for Codex.
|
|
1039
|
+
export const codexAppRuntimeBridge = {
|
|
1040
|
+
id: "codex-app",
|
|
1041
|
+
kind: "codex-app",
|
|
1042
|
+
capabilities: CODEX_APP_CAPABILITIES,
|
|
1043
|
+
supports: (ref, options) => ref?.sdk === "codex" && options?.executionMode === "cli",
|
|
1044
|
+
execute: generateCodexAppResponse,
|
|
1045
|
+
};
|