@voxagent/bridge 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/README.md +74 -0
- package/dist/audio/source.d.ts +23 -0
- package/dist/audio/source.d.ts.map +1 -0
- package/dist/audio/source.js +69 -0
- package/dist/audio/source.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +171 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +102 -0
- package/dist/config.js.map +1 -0
- package/dist/drivers/claude.d.ts +36 -0
- package/dist/drivers/claude.d.ts.map +1 -0
- package/dist/drivers/claude.js +445 -0
- package/dist/drivers/claude.js.map +1 -0
- package/dist/drivers/codex.d.ts +56 -0
- package/dist/drivers/codex.d.ts.map +1 -0
- package/dist/drivers/codex.js +652 -0
- package/dist/drivers/codex.js.map +1 -0
- package/dist/drivers/index.d.ts +34 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +18 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/types.d.ts +81 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +11 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +15 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +36 -0
- package/dist/logging.js.map +1 -0
- package/dist/pair.d.ts +25 -0
- package/dist/pair.d.ts.map +1 -0
- package/dist/pair.js +41 -0
- package/dist/pair.js.map +1 -0
- package/dist/permissions.d.ts +67 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +221 -0
- package/dist/permissions.js.map +1 -0
- package/dist/protocol.d.ts +181 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +63 -0
- package/dist/protocol.js.map +1 -0
- package/dist/room.d.ts +52 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +401 -0
- package/dist/room.js.map +1 -0
- package/dist/tts/elevenlabs.d.ts +43 -0
- package/dist/tts/elevenlabs.d.ts.map +1 -0
- package/dist/tts/elevenlabs.js +268 -0
- package/dist/tts/elevenlabs.js.map +1 -0
- package/dist/tts/index.d.ts +22 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +23 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/tts/openai.d.ts +35 -0
- package/dist/tts/openai.d.ts.map +1 -0
- package/dist/tts/openai.js +270 -0
- package/dist/tts/openai.js.map +1 -0
- package/dist/tts/types.d.ts +28 -0
- package/dist/tts/types.d.ts.map +1 -0
- package/dist/tts/types.js +5 -0
- package/dist/tts/types.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Codex driver — drives `codex app-server` over line-delimited JSON-RPC 2.0.
|
|
3
|
+
*
|
|
4
|
+
* Why app-server instead of `codex exec --json`:
|
|
5
|
+
* `exec --json` does not support pause/resume approvals — when an action
|
|
6
|
+
* would normally trigger a prompt, exec mode treats it as a hard failure.
|
|
7
|
+
* The app-server transport (`codex app-server`, default stdio) is a proper
|
|
8
|
+
* JSON-RPC 2.0 channel where Codex sends server-initiated approval
|
|
9
|
+
* requests (`item/commandExecution/requestApproval`,
|
|
10
|
+
* `item/fileChange/requestApproval`) that we respond to via the same
|
|
11
|
+
* JSON-RPC envelope. That lets us route approvals through the bridge's
|
|
12
|
+
* PermissionBroker to a meeting-side Allow / Deny modal.
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle:
|
|
15
|
+
* - One long-lived `codex app-server` subprocess per CodexDriver instance.
|
|
16
|
+
* Spawned lazily on the first run() so capabilities() stays cheap.
|
|
17
|
+
* - `initialize` request once, `thread/start` on the first run, then
|
|
18
|
+
* `turn/start` for each subsequent run() call (threadId reused so Codex
|
|
19
|
+
* keeps conversation context across turns).
|
|
20
|
+
* - On run abort: `turn/interrupt` for the active turn.
|
|
21
|
+
* - On close(): SIGTERM the process and reject pending requests.
|
|
22
|
+
*
|
|
23
|
+
* The streaming JSON-RPC types and notification methods are taken from
|
|
24
|
+
* `codex app-server generate-ts` output (see /tmp/codex-protocol).
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { readFile } from 'node:fs/promises';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
import { log } from '../logging.js';
|
|
32
|
+
const BRIDGE_VERSION = '0.1.0';
|
|
33
|
+
export class CodexDriver {
|
|
34
|
+
options;
|
|
35
|
+
backend = 'codex';
|
|
36
|
+
proc = null;
|
|
37
|
+
threadId = null;
|
|
38
|
+
nextRequestId = 1;
|
|
39
|
+
pending = new Map();
|
|
40
|
+
activeTurn = null;
|
|
41
|
+
rxBuffer = '';
|
|
42
|
+
serverStartFailed = false;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
}
|
|
46
|
+
async capabilities(cwd) {
|
|
47
|
+
const mcpServers = await this.discoverMcpServers();
|
|
48
|
+
return {
|
|
49
|
+
backend: 'codex',
|
|
50
|
+
skills: [],
|
|
51
|
+
plugins: [],
|
|
52
|
+
mcpServers,
|
|
53
|
+
cwd,
|
|
54
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async *run(input, signal) {
|
|
58
|
+
const runId = randomUUID();
|
|
59
|
+
const startedAtMs = Date.now();
|
|
60
|
+
yield { type: 'run_start', runId, startedAtMs };
|
|
61
|
+
try {
|
|
62
|
+
await this.ensureServer();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
yield {
|
|
66
|
+
type: 'run_end',
|
|
67
|
+
runId,
|
|
68
|
+
reason: 'error',
|
|
69
|
+
message: error instanceof Error ? error.message : String(error),
|
|
70
|
+
totalLatencyMs: Date.now() - startedAtMs,
|
|
71
|
+
};
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!this.threadId) {
|
|
75
|
+
try {
|
|
76
|
+
// Codex 0.104's CLI-side default leaks `gpt-5.5` into thread/start,
|
|
77
|
+
// which the live server rejects. The codex-suffixed flagships
|
|
78
|
+
// (gpt-5.2-codex etc.) require API-key auth and fail under
|
|
79
|
+
// ChatGPT-account auth. Default to `gpt-5.4` which the server
|
|
80
|
+
// advertises in `model/list` and accepts under both auth modes.
|
|
81
|
+
// Users with API keys can override via --llm-model.
|
|
82
|
+
const model = this.options.model ?? 'gpt-5.4';
|
|
83
|
+
const result = (await this.sendRequest('thread/start', {
|
|
84
|
+
cwd: input.cwd,
|
|
85
|
+
approvalPolicy: this.options.fullAccess ? 'never' : 'on-request',
|
|
86
|
+
sandbox: this.options.fullAccess ? 'danger-full-access' : 'workspace-write',
|
|
87
|
+
model,
|
|
88
|
+
experimentalRawEvents: false,
|
|
89
|
+
persistExtendedHistory: false,
|
|
90
|
+
}));
|
|
91
|
+
this.threadId = result.thread?.id ?? null;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
yield {
|
|
95
|
+
type: 'run_end',
|
|
96
|
+
runId,
|
|
97
|
+
reason: 'error',
|
|
98
|
+
message: `thread/start failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
99
|
+
totalLatencyMs: Date.now() - startedAtMs,
|
|
100
|
+
};
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!this.threadId) {
|
|
105
|
+
yield {
|
|
106
|
+
type: 'run_end',
|
|
107
|
+
runId,
|
|
108
|
+
reason: 'error',
|
|
109
|
+
message: 'Codex did not return a thread ID.',
|
|
110
|
+
totalLatencyMs: Date.now() - startedAtMs,
|
|
111
|
+
};
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Build a queue + iterator pair driven by handleNotification + handleServerRequest.
|
|
115
|
+
const queue = [];
|
|
116
|
+
const waiters = [];
|
|
117
|
+
let ended = false;
|
|
118
|
+
const push = (event) => {
|
|
119
|
+
if (ended)
|
|
120
|
+
return;
|
|
121
|
+
const waiter = waiters.shift();
|
|
122
|
+
if (waiter)
|
|
123
|
+
waiter({ value: event, done: false });
|
|
124
|
+
else
|
|
125
|
+
queue.push(event);
|
|
126
|
+
};
|
|
127
|
+
const end = (reason, message) => {
|
|
128
|
+
if (ended)
|
|
129
|
+
return;
|
|
130
|
+
ended = true;
|
|
131
|
+
const totalLatencyMs = Date.now() - startedAtMs;
|
|
132
|
+
const evt = message
|
|
133
|
+
? { type: 'run_end', runId, reason, message, totalLatencyMs }
|
|
134
|
+
: { type: 'run_end', runId, reason, totalLatencyMs };
|
|
135
|
+
const waiter = waiters.shift();
|
|
136
|
+
if (waiter)
|
|
137
|
+
waiter({ value: evt, done: false });
|
|
138
|
+
else
|
|
139
|
+
queue.push(evt);
|
|
140
|
+
// Then signal exhaustion.
|
|
141
|
+
const final = waiters.shift();
|
|
142
|
+
if (final)
|
|
143
|
+
final({ value: undefined, done: true });
|
|
144
|
+
};
|
|
145
|
+
const turn = {
|
|
146
|
+
turnId: null,
|
|
147
|
+
push,
|
|
148
|
+
end,
|
|
149
|
+
startedAtMs,
|
|
150
|
+
itemStarts: new Map(),
|
|
151
|
+
runId,
|
|
152
|
+
signal,
|
|
153
|
+
finalText: '',
|
|
154
|
+
};
|
|
155
|
+
this.activeTurn = turn;
|
|
156
|
+
let promptText = input.prompt;
|
|
157
|
+
if (input.systemPromptAddendum) {
|
|
158
|
+
promptText = `${input.systemPromptAddendum}\n\n---\n\n${promptText}`;
|
|
159
|
+
}
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
if (!turn.turnId)
|
|
162
|
+
return;
|
|
163
|
+
void this.sendRequest('turn/interrupt', { threadId: this.threadId, turnId: turn.turnId }).catch(() => undefined);
|
|
164
|
+
};
|
|
165
|
+
if (signal.aborted)
|
|
166
|
+
onAbort();
|
|
167
|
+
else
|
|
168
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
169
|
+
// Kick off the turn — don't await, let notifications stream while we wait.
|
|
170
|
+
void this.sendRequest('turn/start', {
|
|
171
|
+
threadId: this.threadId,
|
|
172
|
+
input: [{ type: 'text', text: promptText }],
|
|
173
|
+
})
|
|
174
|
+
.then((result) => {
|
|
175
|
+
const r = result;
|
|
176
|
+
if (r.turn?.id)
|
|
177
|
+
turn.turnId = r.turn.id;
|
|
178
|
+
})
|
|
179
|
+
.catch((error) => {
|
|
180
|
+
end('error', `turn/start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
while (true) {
|
|
184
|
+
if (queue.length > 0) {
|
|
185
|
+
const evt = queue.shift();
|
|
186
|
+
yield evt;
|
|
187
|
+
if (evt.type === 'run_end')
|
|
188
|
+
return;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ended && queue.length === 0)
|
|
192
|
+
return;
|
|
193
|
+
const next = await new Promise((resolve) => {
|
|
194
|
+
waiters.push(resolve);
|
|
195
|
+
});
|
|
196
|
+
if (next.done)
|
|
197
|
+
return;
|
|
198
|
+
yield next.value;
|
|
199
|
+
if (next.value.type === 'run_end')
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
signal.removeEventListener('abort', onAbort);
|
|
205
|
+
if (this.activeTurn === turn)
|
|
206
|
+
this.activeTurn = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async close() {
|
|
210
|
+
if (this.activeTurn) {
|
|
211
|
+
this.activeTurn.end('interrupted');
|
|
212
|
+
this.activeTurn = null;
|
|
213
|
+
}
|
|
214
|
+
for (const [, entry] of this.pending) {
|
|
215
|
+
entry.reject(new Error('Codex driver closed.'));
|
|
216
|
+
}
|
|
217
|
+
this.pending.clear();
|
|
218
|
+
if (this.proc && !this.proc.killed) {
|
|
219
|
+
this.proc.kill('SIGTERM');
|
|
220
|
+
}
|
|
221
|
+
this.proc = null;
|
|
222
|
+
this.threadId = null;
|
|
223
|
+
}
|
|
224
|
+
async ensureServer() {
|
|
225
|
+
if (this.proc)
|
|
226
|
+
return;
|
|
227
|
+
if (this.serverStartFailed) {
|
|
228
|
+
throw new Error('codex app-server previously failed to start.');
|
|
229
|
+
}
|
|
230
|
+
const child = spawn('codex', ['app-server'], {
|
|
231
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
232
|
+
env: { ...process.env },
|
|
233
|
+
});
|
|
234
|
+
this.proc = child;
|
|
235
|
+
child.stdout.setEncoding('utf-8');
|
|
236
|
+
child.stdout.on('data', (chunk) => this.onStdout(chunk));
|
|
237
|
+
child.stderr.setEncoding('utf-8');
|
|
238
|
+
child.stderr.on('data', (chunk) => {
|
|
239
|
+
log.debug('codex_stderr', { chunk: chunk.slice(0, 240) });
|
|
240
|
+
});
|
|
241
|
+
child.once('exit', (code, signal) => {
|
|
242
|
+
log.info('codex_app_server_exited', { code, signal });
|
|
243
|
+
const wasActive = this.activeTurn;
|
|
244
|
+
this.proc = null;
|
|
245
|
+
for (const [, entry] of this.pending) {
|
|
246
|
+
entry.reject(new Error('codex app-server exited.'));
|
|
247
|
+
}
|
|
248
|
+
this.pending.clear();
|
|
249
|
+
if (wasActive) {
|
|
250
|
+
wasActive.end('error', 'codex app-server exited unexpectedly.');
|
|
251
|
+
this.activeTurn = null;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
try {
|
|
255
|
+
await this.sendRequest('initialize', {
|
|
256
|
+
clientInfo: { name: 'voxagent-bridge', title: 'VoxAgent Bridge', version: BRIDGE_VERSION },
|
|
257
|
+
capabilities: { experimentalApi: false },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.serverStartFailed = true;
|
|
262
|
+
child.kill('SIGTERM');
|
|
263
|
+
this.proc = null;
|
|
264
|
+
throw new Error(`codex initialize failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
onStdout(chunk) {
|
|
268
|
+
this.rxBuffer += chunk;
|
|
269
|
+
let idx;
|
|
270
|
+
while ((idx = this.rxBuffer.indexOf('\n')) !== -1) {
|
|
271
|
+
const line = this.rxBuffer.slice(0, idx).trim();
|
|
272
|
+
this.rxBuffer = this.rxBuffer.slice(idx + 1);
|
|
273
|
+
if (line.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(line);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
log.warn('codex_unparseable_line', { line: line.slice(0, 200) });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
this.dispatch(parsed);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
dispatch(msg) {
|
|
287
|
+
if ('id' in msg && msg.result !== undefined) {
|
|
288
|
+
this.handleResponse(msg);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if ('id' in msg && msg.error !== undefined) {
|
|
292
|
+
this.handleResponse(msg);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if ('id' in msg && 'method' in msg) {
|
|
296
|
+
this.handleServerRequest(msg);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if ('method' in msg) {
|
|
300
|
+
this.handleNotification(msg);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
handleResponse(resp) {
|
|
305
|
+
if (typeof resp.id !== 'number')
|
|
306
|
+
return;
|
|
307
|
+
const entry = this.pending.get(resp.id);
|
|
308
|
+
if (!entry)
|
|
309
|
+
return;
|
|
310
|
+
this.pending.delete(resp.id);
|
|
311
|
+
if (resp.error) {
|
|
312
|
+
entry.reject(new Error(`${resp.error.code}: ${resp.error.message}`));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
entry.resolve(resp.result ?? null);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
handleServerRequest(req) {
|
|
319
|
+
const params = (req.params ?? {});
|
|
320
|
+
if (req.method === 'item/commandExecution/requestApproval') {
|
|
321
|
+
void this.handleApproval('Bash', req.id, params);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (req.method === 'item/fileChange/requestApproval') {
|
|
325
|
+
void this.handleApproval('Edit', req.id, params);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// For server requests we don't recognize, decline politely so Codex doesn't hang.
|
|
329
|
+
log.debug('codex_unknown_server_request', { method: req.method });
|
|
330
|
+
this.sendResponse(req.id, { error: { code: -32601, message: 'Not implemented in bridge.' } });
|
|
331
|
+
}
|
|
332
|
+
async handleApproval(surface, requestId, params) {
|
|
333
|
+
const resolver = this.options.permissionResolver;
|
|
334
|
+
if (this.options.fullAccess || !resolver) {
|
|
335
|
+
this.sendResponse(requestId, { result: { decision: 'accept' } });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const result = await resolver.decide({
|
|
340
|
+
runId: this.activeTurn?.runId ?? 'unknown',
|
|
341
|
+
toolName: surface,
|
|
342
|
+
toolInput: params,
|
|
343
|
+
});
|
|
344
|
+
this.sendResponse(requestId, {
|
|
345
|
+
result: { decision: result.decision === 'allow' ? 'accept' : 'decline' },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
log.warn('codex_approval_resolver_failed', {
|
|
350
|
+
message: error instanceof Error ? error.message : String(error),
|
|
351
|
+
});
|
|
352
|
+
this.sendResponse(requestId, { result: { decision: 'decline' } });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
handleNotification(notif) {
|
|
356
|
+
const turn = this.activeTurn;
|
|
357
|
+
const params = (notif.params ?? {});
|
|
358
|
+
switch (notif.method) {
|
|
359
|
+
case 'turn/started': {
|
|
360
|
+
if (!turn)
|
|
361
|
+
return;
|
|
362
|
+
const p = params;
|
|
363
|
+
turn.turnId = p.turn.id;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
case 'turn/completed': {
|
|
367
|
+
if (!turn)
|
|
368
|
+
return;
|
|
369
|
+
const p = params;
|
|
370
|
+
if (p.turn?.id && turn.turnId && p.turn.id !== turn.turnId)
|
|
371
|
+
return;
|
|
372
|
+
if (turn.finalText) {
|
|
373
|
+
turn.push({ type: 'text_done', runId: turn.runId, text: turn.finalText });
|
|
374
|
+
}
|
|
375
|
+
const failed = p.turn?.status === 'failed' || p.turn?.status === 'error';
|
|
376
|
+
const reason = turn.signal.aborted
|
|
377
|
+
? 'interrupted'
|
|
378
|
+
: failed
|
|
379
|
+
? 'error'
|
|
380
|
+
: 'completed';
|
|
381
|
+
const errorMessage = parseTurnErrorMessage(p.turn?.error?.message);
|
|
382
|
+
if (failed && errorMessage) {
|
|
383
|
+
// Surface the failure to the room so the user sees why the agent
|
|
384
|
+
// went idle rather than just watching status drop silently.
|
|
385
|
+
turn.push({
|
|
386
|
+
type: 'text_done',
|
|
387
|
+
runId: turn.runId,
|
|
388
|
+
text: `Codex couldn't complete the turn: ${errorMessage}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
turn.end(reason, errorMessage ?? undefined);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
case 'item/started': {
|
|
395
|
+
if (!turn)
|
|
396
|
+
return;
|
|
397
|
+
const p = params;
|
|
398
|
+
this.handleItemStarted(turn, p);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
case 'item/completed': {
|
|
402
|
+
if (!turn)
|
|
403
|
+
return;
|
|
404
|
+
const p = params;
|
|
405
|
+
this.handleItemCompleted(turn, p);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
case 'item/agentMessage/delta': {
|
|
409
|
+
if (!turn)
|
|
410
|
+
return;
|
|
411
|
+
const p = params;
|
|
412
|
+
if (p.delta && p.delta.length > 0) {
|
|
413
|
+
turn.finalText += p.delta;
|
|
414
|
+
turn.push({ type: 'text_delta', runId: turn.runId, delta: p.delta });
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
case 'item/commandExecution/outputDelta': {
|
|
419
|
+
if (!turn)
|
|
420
|
+
return;
|
|
421
|
+
const p = params;
|
|
422
|
+
const text = p.text ?? p.chunk ?? '';
|
|
423
|
+
if (text.length > 0) {
|
|
424
|
+
turn.push({
|
|
425
|
+
type: 'tool_use_progress',
|
|
426
|
+
runId: turn.runId,
|
|
427
|
+
toolUseId: p.itemId,
|
|
428
|
+
chunk: text,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
case 'item/reasoning/textDelta':
|
|
434
|
+
case 'item/reasoning/summaryTextDelta': {
|
|
435
|
+
if (!turn)
|
|
436
|
+
return;
|
|
437
|
+
const p = params;
|
|
438
|
+
if (p.delta) {
|
|
439
|
+
turn.push({ type: 'thinking_delta', runId: turn.runId, delta: p.delta });
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
case 'error': {
|
|
444
|
+
if (!turn)
|
|
445
|
+
return;
|
|
446
|
+
const errMessage = typeof params.error?.message === 'string'
|
|
447
|
+
? parseTurnErrorMessage(params.error.message)
|
|
448
|
+
: null;
|
|
449
|
+
const message = errMessage ?? (typeof params.message === 'string' ? params.message : 'Codex error.');
|
|
450
|
+
turn.push({
|
|
451
|
+
type: 'text_done',
|
|
452
|
+
runId: turn.runId,
|
|
453
|
+
text: `Codex error: ${message}`,
|
|
454
|
+
});
|
|
455
|
+
turn.end('error', message);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
default:
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
handleItemStarted(turn, params) {
|
|
463
|
+
const item = params.item;
|
|
464
|
+
if (item.type === 'commandExecution') {
|
|
465
|
+
const ce = item;
|
|
466
|
+
turn.itemStarts.set(item.id, Date.now());
|
|
467
|
+
turn.push({
|
|
468
|
+
type: 'tool_use_start',
|
|
469
|
+
runId: turn.runId,
|
|
470
|
+
toolUseId: item.id,
|
|
471
|
+
name: 'Bash',
|
|
472
|
+
input: { command: ce.command, cwd: ce.cwd },
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (item.type === 'fileChange') {
|
|
477
|
+
const fc = item;
|
|
478
|
+
turn.itemStarts.set(item.id, Date.now());
|
|
479
|
+
turn.push({
|
|
480
|
+
type: 'tool_use_start',
|
|
481
|
+
runId: turn.runId,
|
|
482
|
+
toolUseId: item.id,
|
|
483
|
+
name: 'Edit',
|
|
484
|
+
input: { changes: fc.changes },
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (item.type === 'mcpToolCall') {
|
|
489
|
+
const mc = item;
|
|
490
|
+
turn.itemStarts.set(item.id, Date.now());
|
|
491
|
+
turn.push({
|
|
492
|
+
type: 'tool_use_start',
|
|
493
|
+
runId: turn.runId,
|
|
494
|
+
toolUseId: item.id,
|
|
495
|
+
name: `${mc.server}:${mc.tool}`,
|
|
496
|
+
input: mc.arguments,
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (item.type === 'webSearch') {
|
|
501
|
+
const ws = item;
|
|
502
|
+
turn.itemStarts.set(item.id, Date.now());
|
|
503
|
+
turn.push({
|
|
504
|
+
type: 'tool_use_start',
|
|
505
|
+
runId: turn.runId,
|
|
506
|
+
toolUseId: item.id,
|
|
507
|
+
name: 'WebSearch',
|
|
508
|
+
input: { query: ws.query },
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
handleItemCompleted(turn, params) {
|
|
513
|
+
const item = params.item;
|
|
514
|
+
const startedAt = turn.itemStarts.get(item.id);
|
|
515
|
+
const latencyMs = startedAt ? Date.now() - startedAt : undefined;
|
|
516
|
+
if (item.type === 'agentMessage') {
|
|
517
|
+
const am = item;
|
|
518
|
+
// Final assistant text — prefer the streamed accumulator, fall back to am.text.
|
|
519
|
+
if (!turn.finalText && am.text)
|
|
520
|
+
turn.finalText = am.text;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (item.type === 'commandExecution') {
|
|
524
|
+
const ce = item;
|
|
525
|
+
const status = ce.exitCode === 0 ? 'success' : 'error';
|
|
526
|
+
turn.push({
|
|
527
|
+
type: 'tool_use_end',
|
|
528
|
+
runId: turn.runId,
|
|
529
|
+
toolUseId: item.id,
|
|
530
|
+
status,
|
|
531
|
+
output: {
|
|
532
|
+
command: ce.command,
|
|
533
|
+
aggregatedOutput: ce.aggregatedOutput,
|
|
534
|
+
exitCode: ce.exitCode,
|
|
535
|
+
durationMs: ce.durationMs,
|
|
536
|
+
},
|
|
537
|
+
...(latencyMs !== undefined ? { latencyMs } : {}),
|
|
538
|
+
});
|
|
539
|
+
turn.itemStarts.delete(item.id);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (item.type === 'fileChange') {
|
|
543
|
+
const fc = item;
|
|
544
|
+
turn.push({
|
|
545
|
+
type: 'tool_use_end',
|
|
546
|
+
runId: turn.runId,
|
|
547
|
+
toolUseId: item.id,
|
|
548
|
+
status: fc.status === 'success' || fc.status === 'completed' ? 'success' : 'error',
|
|
549
|
+
output: { changes: fc.changes, status: fc.status },
|
|
550
|
+
...(latencyMs !== undefined ? { latencyMs } : {}),
|
|
551
|
+
});
|
|
552
|
+
turn.itemStarts.delete(item.id);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (item.type === 'mcpToolCall') {
|
|
556
|
+
const mc = item;
|
|
557
|
+
turn.push({
|
|
558
|
+
type: 'tool_use_end',
|
|
559
|
+
runId: turn.runId,
|
|
560
|
+
toolUseId: item.id,
|
|
561
|
+
status: mc.error ? 'error' : 'success',
|
|
562
|
+
output: { result: mc.result, error: mc.error },
|
|
563
|
+
...(latencyMs !== undefined ? { latencyMs } : {}),
|
|
564
|
+
});
|
|
565
|
+
turn.itemStarts.delete(item.id);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (item.type === 'webSearch') {
|
|
569
|
+
turn.push({
|
|
570
|
+
type: 'tool_use_end',
|
|
571
|
+
runId: turn.runId,
|
|
572
|
+
toolUseId: item.id,
|
|
573
|
+
status: 'success',
|
|
574
|
+
output: item,
|
|
575
|
+
...(latencyMs !== undefined ? { latencyMs } : {}),
|
|
576
|
+
});
|
|
577
|
+
turn.itemStarts.delete(item.id);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
sendRequest(method, params) {
|
|
581
|
+
if (!this.proc) {
|
|
582
|
+
return Promise.reject(new Error('codex app-server is not running.'));
|
|
583
|
+
}
|
|
584
|
+
const id = this.nextRequestId++;
|
|
585
|
+
const message = { jsonrpc: '2.0', id, method, params };
|
|
586
|
+
return new Promise((resolve, reject) => {
|
|
587
|
+
this.pending.set(id, { resolve, reject });
|
|
588
|
+
try {
|
|
589
|
+
this.proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
this.pending.delete(id);
|
|
593
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
sendResponse(id, body) {
|
|
598
|
+
if (!this.proc)
|
|
599
|
+
return;
|
|
600
|
+
const envelope = { jsonrpc: '2.0', id, ...body };
|
|
601
|
+
try {
|
|
602
|
+
this.proc.stdin.write(`${JSON.stringify(envelope)}\n`);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
log.warn('codex_send_response_failed', {
|
|
606
|
+
message: error instanceof Error ? error.message : String(error),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async discoverMcpServers() {
|
|
611
|
+
const configPath = join(homedir(), '.codex', 'config.toml');
|
|
612
|
+
try {
|
|
613
|
+
const text = await readFile(configPath, 'utf-8');
|
|
614
|
+
const found = new Set();
|
|
615
|
+
const sectionRegex = /^\s*\[mcp_servers\.([^\]\s]+)\]/gm;
|
|
616
|
+
let match;
|
|
617
|
+
while ((match = sectionRegex.exec(text)) !== null) {
|
|
618
|
+
if (match[1])
|
|
619
|
+
found.add(match[1]);
|
|
620
|
+
}
|
|
621
|
+
return [...found].sort();
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Codex sometimes wraps its server-side errors as a JSON string inside
|
|
630
|
+
* `turn.error.message` — e.g. `'{"detail":"…"}'`. Pull out the human-readable
|
|
631
|
+
* sentence so we don't dump raw JSON into the meeting UI.
|
|
632
|
+
*/
|
|
633
|
+
function parseTurnErrorMessage(raw) {
|
|
634
|
+
if (typeof raw !== 'string')
|
|
635
|
+
return null;
|
|
636
|
+
const trimmed = raw.trim();
|
|
637
|
+
if (!trimmed)
|
|
638
|
+
return null;
|
|
639
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
640
|
+
try {
|
|
641
|
+
const parsed = JSON.parse(trimmed);
|
|
642
|
+
const detail = parsed.detail ?? parsed.message;
|
|
643
|
+
if (typeof detail === 'string' && detail.length > 0)
|
|
644
|
+
return detail;
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// fall through to raw string
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return trimmed;
|
|
651
|
+
}
|
|
652
|
+
//# sourceMappingURL=codex.js.map
|