@ai-sdk/harness-codex 1.0.42 → 1.0.43
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/CHANGELOG.md +10 -0
- package/dist/bridge/index.mjs +201 -192
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +287 -0
- package/src/bridge/index.ts +13 -292
- package/src/codex-harness.ts +5 -5
package/src/bridge/index.ts
CHANGED
|
@@ -14,9 +14,7 @@ import {
|
|
|
14
14
|
type BridgeEvent,
|
|
15
15
|
type BridgeTurn,
|
|
16
16
|
} from '@ai-sdk/harness/bridge';
|
|
17
|
-
import type { HarnessV1BuiltinToolName } from '@ai-sdk/harness';
|
|
18
17
|
import type { StartMessage } from '../codex-bridge-protocol';
|
|
19
|
-
import { randomUUID } from 'node:crypto';
|
|
20
18
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
21
19
|
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
22
20
|
import {
|
|
@@ -24,11 +22,11 @@ import {
|
|
|
24
22
|
buildCliShimScript,
|
|
25
23
|
parseToolRelayCommands,
|
|
26
24
|
} from './cli-relay';
|
|
25
|
+
import { createCodexStepTracker, defaultUsage } from './codex-step-tracker';
|
|
27
26
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from './codex-step-tracker';
|
|
27
|
+
createEmitStreamEvent,
|
|
28
|
+
type CodexEvent,
|
|
29
|
+
} from './create-emit-stream-event';
|
|
32
30
|
import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
|
|
33
31
|
import { argv, env as procEnv, stdout } from 'node:process';
|
|
34
32
|
|
|
@@ -51,20 +49,6 @@ import { argv, env as procEnv, stdout } from 'node:process';
|
|
|
51
49
|
*/
|
|
52
50
|
import * as codexSdkModule from '@openai/codex-sdk';
|
|
53
51
|
|
|
54
|
-
/*
|
|
55
|
-
* Native Codex tool name → cross-harness common name. Tools outside this map
|
|
56
|
-
* (e.g. MCP tools the model invokes by name) have no common equivalent; their
|
|
57
|
-
* native name is forwarded as-is on `tool-call` events.
|
|
58
|
-
*/
|
|
59
|
-
const NATIVE_TO_COMMON: Readonly<Record<string, HarnessV1BuiltinToolName>> = {
|
|
60
|
-
shell: 'bash',
|
|
61
|
-
web_search: 'webSearch',
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
function toCommonName(nativeName: string): HarnessV1BuiltinToolName | string {
|
|
65
|
-
return NATIVE_TO_COMMON[nativeName] ?? nativeName;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
52
|
const args = parseArgs(argv.slice(2));
|
|
69
53
|
const workdir = requireArg({ value: args.workdir, name: '--workdir' });
|
|
70
54
|
const bridgeStateDir = requireArg({
|
|
@@ -202,9 +186,15 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
202
186
|
|
|
203
187
|
const userMessage = start.prompt;
|
|
204
188
|
let turnUsage: Record<string, unknown> | undefined;
|
|
205
|
-
const textByItem = new Map<string, string>();
|
|
206
|
-
const reasoningByItem = new Map<string, string>();
|
|
207
189
|
const stepTracker = createCodexStepTracker({ send: emit });
|
|
190
|
+
const emitStreamEvent = createEmitStreamEvent({
|
|
191
|
+
send: emit,
|
|
192
|
+
stepTracker,
|
|
193
|
+
setTurnUsage: usage => (turnUsage = usage),
|
|
194
|
+
setThreadId: threadId => (threadState.id = threadId),
|
|
195
|
+
emitWarning: turn.emitWarning,
|
|
196
|
+
emitError: turn.emitError,
|
|
197
|
+
});
|
|
208
198
|
|
|
209
199
|
try {
|
|
210
200
|
const { events } = await thread.runStreamed(userMessage, {
|
|
@@ -212,14 +202,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
212
202
|
});
|
|
213
203
|
for await (const event of events as AsyncIterable<CodexEvent>) {
|
|
214
204
|
if (turn.abortSignal.aborted) break;
|
|
215
|
-
if (
|
|
216
|
-
event.type === 'thread.started' &&
|
|
217
|
-
typeof event.thread_id === 'string'
|
|
218
|
-
) {
|
|
219
|
-
threadState.id = event.thread_id;
|
|
220
|
-
// Announce to the host so it can include the id in resume state.
|
|
221
|
-
emit({ type: 'bridge-thread', threadId: event.thread_id });
|
|
222
|
-
}
|
|
223
205
|
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
224
206
|
if (cliShimPath && event.item?.type === 'command_execution') {
|
|
225
207
|
const relayCalls =
|
|
@@ -239,15 +221,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
239
221
|
continue;
|
|
240
222
|
}
|
|
241
223
|
}
|
|
242
|
-
|
|
243
|
-
send: emit,
|
|
244
|
-
textByItem,
|
|
245
|
-
reasoningByItem,
|
|
246
|
-
stepTracker,
|
|
247
|
-
setTurnUsage: u => (turnUsage = u),
|
|
248
|
-
emitWarning: turn.emitWarning,
|
|
249
|
-
emitError: turn.emitError,
|
|
250
|
-
});
|
|
224
|
+
emitStreamEvent(event);
|
|
251
225
|
}
|
|
252
226
|
} catch (err) {
|
|
253
227
|
turn.emitError({ error: err, message: 'codex turn failed' });
|
|
@@ -265,259 +239,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
265
239
|
void turn.pendingUserMessages; // accepted but only consumed when codex supports streamed user input
|
|
266
240
|
}
|
|
267
241
|
|
|
268
|
-
type CodexItem = {
|
|
269
|
-
type: string;
|
|
270
|
-
id?: string;
|
|
271
|
-
text?: string;
|
|
272
|
-
command?: string;
|
|
273
|
-
exit_code?: number;
|
|
274
|
-
aggregated_output?: string;
|
|
275
|
-
status?: 'in_progress' | 'completed' | 'failed';
|
|
276
|
-
server?: string;
|
|
277
|
-
tool?: string;
|
|
278
|
-
arguments?: unknown;
|
|
279
|
-
result?: { content?: unknown; structured_content?: unknown } | unknown;
|
|
280
|
-
error?: { message?: string };
|
|
281
|
-
query?: string;
|
|
282
|
-
message?: string;
|
|
283
|
-
changes?: ReadonlyArray<{
|
|
284
|
-
path: string;
|
|
285
|
-
kind: 'add' | 'delete' | 'update';
|
|
286
|
-
}>;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
function extractMcpToolCallResult(item: CodexItem): unknown {
|
|
290
|
-
if (
|
|
291
|
-
item.result === undefined ||
|
|
292
|
-
item.result === null ||
|
|
293
|
-
typeof item.result !== 'object'
|
|
294
|
-
) {
|
|
295
|
-
return item.error?.message ? { error: item.error.message } : null;
|
|
296
|
-
}
|
|
297
|
-
const result = item.result as {
|
|
298
|
-
content?: unknown;
|
|
299
|
-
structured_content?: unknown;
|
|
300
|
-
};
|
|
301
|
-
if (
|
|
302
|
-
result.structured_content !== undefined &&
|
|
303
|
-
result.structured_content !== null
|
|
304
|
-
) {
|
|
305
|
-
return result.structured_content;
|
|
306
|
-
}
|
|
307
|
-
return result.content ?? null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
type CodexEvent = {
|
|
311
|
-
type:
|
|
312
|
-
| 'thread.started'
|
|
313
|
-
| 'turn.completed'
|
|
314
|
-
| 'turn.failed'
|
|
315
|
-
| 'error'
|
|
316
|
-
| 'item.started'
|
|
317
|
-
| 'item.updated'
|
|
318
|
-
| 'item.completed';
|
|
319
|
-
item?: CodexItem;
|
|
320
|
-
usage?: Record<string, number>;
|
|
321
|
-
error?: { message: string };
|
|
322
|
-
message?: string;
|
|
323
|
-
thread_id?: string;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
function translateAndEmit(
|
|
327
|
-
event: CodexEvent,
|
|
328
|
-
ctx: {
|
|
329
|
-
send: Emit;
|
|
330
|
-
textByItem: Map<string, string>;
|
|
331
|
-
reasoningByItem: Map<string, string>;
|
|
332
|
-
stepTracker: CodexStepTracker;
|
|
333
|
-
setTurnUsage: (u: Record<string, unknown>) => void;
|
|
334
|
-
emitWarning: BridgeTurn['emitWarning'];
|
|
335
|
-
emitError: BridgeTurn['emitError'];
|
|
336
|
-
},
|
|
337
|
-
): void {
|
|
338
|
-
if (event.type === 'turn.completed') {
|
|
339
|
-
if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
|
|
340
|
-
ctx.stepTracker.finishStep();
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (event.type === 'turn.failed') {
|
|
344
|
-
ctx.emitError({
|
|
345
|
-
error: event.error?.message ?? 'codex turn failed',
|
|
346
|
-
message: 'codex turn failed',
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (event.type === 'error') {
|
|
351
|
-
ctx.emitError({
|
|
352
|
-
error: event.message ?? 'codex error',
|
|
353
|
-
message: 'codex stream error',
|
|
354
|
-
});
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (!event.item) return;
|
|
358
|
-
const item = event.item;
|
|
359
|
-
const id = item.id ?? randomUUID();
|
|
360
|
-
const observeStep = (): void => {
|
|
361
|
-
ctx.stepTracker.observeEvent({ event, itemId: id });
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
if (item.type === 'agent_message' && typeof item.text === 'string') {
|
|
365
|
-
/*
|
|
366
|
-
* The presence of `id` in `textByItem` — not the `item.started` event —
|
|
367
|
-
* marks the text part as opened. Codex does not guarantee an
|
|
368
|
-
* `item.started` event carrying text precedes the first `item.updated`
|
|
369
|
-
* with text, so keying the `text-start` off the event type can emit a
|
|
370
|
-
* `text-delta` for a part that was never opened. Opening lazily on the
|
|
371
|
-
* first event with text keeps `text-start` before any `text-delta`.
|
|
372
|
-
*/
|
|
373
|
-
if (!ctx.textByItem.has(id)) {
|
|
374
|
-
ctx.send({ type: 'text-start', id });
|
|
375
|
-
ctx.textByItem.set(id, '');
|
|
376
|
-
}
|
|
377
|
-
const last = ctx.textByItem.get(id) ?? '';
|
|
378
|
-
const next = item.text;
|
|
379
|
-
if (next.length > last.length) {
|
|
380
|
-
ctx.send({ type: 'text-delta', id, delta: next.slice(last.length) });
|
|
381
|
-
ctx.textByItem.set(id, next);
|
|
382
|
-
}
|
|
383
|
-
if (event.type === 'item.completed') ctx.send({ type: 'text-end', id });
|
|
384
|
-
observeStep();
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (item.type === 'reasoning' && typeof item.text === 'string') {
|
|
389
|
-
if (!ctx.reasoningByItem.has(id)) {
|
|
390
|
-
ctx.send({ type: 'reasoning-start', id });
|
|
391
|
-
ctx.reasoningByItem.set(id, '');
|
|
392
|
-
}
|
|
393
|
-
const last = ctx.reasoningByItem.get(id) ?? '';
|
|
394
|
-
const next = item.text;
|
|
395
|
-
if (next.length > last.length) {
|
|
396
|
-
ctx.send({ type: 'reasoning-delta', id, delta: next.slice(last.length) });
|
|
397
|
-
ctx.reasoningByItem.set(id, next);
|
|
398
|
-
}
|
|
399
|
-
if (event.type === 'item.completed')
|
|
400
|
-
ctx.send({ type: 'reasoning-end', id });
|
|
401
|
-
observeStep();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (item.type === 'command_execution') {
|
|
406
|
-
const nativeName = 'shell';
|
|
407
|
-
if (event.type === 'item.started') {
|
|
408
|
-
ctx.send({
|
|
409
|
-
type: 'tool-call',
|
|
410
|
-
toolCallId: id,
|
|
411
|
-
toolName: toCommonName(nativeName),
|
|
412
|
-
nativeName,
|
|
413
|
-
input: JSON.stringify({ command: item.command ?? '' }),
|
|
414
|
-
providerExecuted: true,
|
|
415
|
-
});
|
|
416
|
-
} else if (event.type === 'item.completed') {
|
|
417
|
-
ctx.send({
|
|
418
|
-
type: 'tool-result',
|
|
419
|
-
toolCallId: id,
|
|
420
|
-
toolName: toCommonName(nativeName),
|
|
421
|
-
result: {
|
|
422
|
-
exitCode: item.exit_code ?? null,
|
|
423
|
-
output: item.aggregated_output ?? '',
|
|
424
|
-
status: item.status ?? 'completed',
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
observeStep();
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (item.type === 'mcp_tool_call') {
|
|
433
|
-
if (event.type === 'item.started') {
|
|
434
|
-
ctx.send({
|
|
435
|
-
type: 'tool-call',
|
|
436
|
-
toolCallId: id,
|
|
437
|
-
toolName: item.tool ?? 'unknown',
|
|
438
|
-
nativeName: item.tool ?? 'unknown',
|
|
439
|
-
input: JSON.stringify(item.arguments ?? {}),
|
|
440
|
-
providerExecuted: true,
|
|
441
|
-
});
|
|
442
|
-
} else if (event.type === 'item.completed') {
|
|
443
|
-
ctx.send({
|
|
444
|
-
type: 'tool-result',
|
|
445
|
-
toolCallId: id,
|
|
446
|
-
toolName: item.tool ?? 'unknown',
|
|
447
|
-
result: extractMcpToolCallResult(item),
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
observeStep();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (item.type === 'web_search') {
|
|
455
|
-
const nativeName = 'web_search';
|
|
456
|
-
if (event.type === 'item.started') {
|
|
457
|
-
ctx.send({
|
|
458
|
-
type: 'tool-call',
|
|
459
|
-
toolCallId: id,
|
|
460
|
-
toolName: toCommonName(nativeName),
|
|
461
|
-
nativeName,
|
|
462
|
-
input: JSON.stringify({ query: item.query ?? '' }),
|
|
463
|
-
providerExecuted: true,
|
|
464
|
-
});
|
|
465
|
-
} else if (event.type === 'item.completed') {
|
|
466
|
-
ctx.send({
|
|
467
|
-
type: 'tool-result',
|
|
468
|
-
toolCallId: id,
|
|
469
|
-
toolName: toCommonName(nativeName),
|
|
470
|
-
result: item.result ?? null,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
observeStep();
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (item.type === 'file_change' && event.type === 'item.completed') {
|
|
478
|
-
for (const change of item.changes ?? []) {
|
|
479
|
-
ctx.send({
|
|
480
|
-
type: 'file-change',
|
|
481
|
-
event:
|
|
482
|
-
change.kind === 'add'
|
|
483
|
-
? 'create'
|
|
484
|
-
: change.kind === 'delete'
|
|
485
|
-
? 'delete'
|
|
486
|
-
: 'modify',
|
|
487
|
-
path: change.path,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
observeStep();
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (item.type === 'error' && event.type === 'item.completed') {
|
|
495
|
-
const message =
|
|
496
|
-
typeof item.message === 'string' && item.message.trim()
|
|
497
|
-
? item.message
|
|
498
|
-
: 'codex reported a non-fatal error item';
|
|
499
|
-
ctx.emitWarning({ message });
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function mapUsage(usage: Record<string, number>): Record<string, unknown> {
|
|
505
|
-
const input = usage.input_tokens ?? 0;
|
|
506
|
-
const cacheRead = usage.cached_input_tokens ?? 0;
|
|
507
|
-
return {
|
|
508
|
-
inputTokens: {
|
|
509
|
-
total: input,
|
|
510
|
-
noCache: Math.max(0, input - cacheRead),
|
|
511
|
-
cacheRead,
|
|
512
|
-
cacheWrite: 0,
|
|
513
|
-
},
|
|
514
|
-
outputTokens: {
|
|
515
|
-
total: usage.output_tokens ?? 0,
|
|
516
|
-
text: usage.output_tokens ?? 0,
|
|
517
|
-
},
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
242
|
/**
|
|
522
243
|
* Tool relay — HTTP server on 127.0.0.1:0. The CLI shim invoked by Codex POSTs
|
|
523
244
|
* each tool invocation here; the relay forwards the call to the host (via the
|
package/src/codex-harness.ts
CHANGED
|
@@ -60,12 +60,12 @@ type WriteSkillsResult = {
|
|
|
60
60
|
* The model the adapter pins when the consumer configures none. The Codex SDK
|
|
61
61
|
* does not report the model it resolves to at runtime (no model field on any
|
|
62
62
|
* event), and exposes no default-model constant, so we pin the latest
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* default model resolved by the bundled `@openai/codex@0.144.5`:
|
|
64
|
+
* `gpt-5.6-sol`. Keep this in sync when bumping the codex SDK/binary. Passing
|
|
65
|
+
* it explicitly makes the resolved model deterministic and the telemetry
|
|
66
|
+
* (`gen_ai.request.model`) accurate.
|
|
67
67
|
*/
|
|
68
|
-
const DEFAULT_CODEX_MODEL = 'gpt-5.
|
|
68
|
+
const DEFAULT_CODEX_MODEL = 'gpt-5.6-sol';
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Value to use in User-Agent and `x-client-app` headers.
|