@ai-sdk/harness-codex 1.0.125 → 1.0.128

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.
@@ -0,0 +1,531 @@
1
+ import type { BridgeTurn } from '@ai-sdk/harness/bridge';
2
+ import type { CodexStepTracker } from './codex-step-tracker';
3
+ import type { CodexEvent, CodexItem } from './create-emit-stream-event';
4
+ import type { CodexAppServerNotification } from './codex-app-server-client';
5
+
6
+ type UsageBreakdown = {
7
+ inputTokens: number;
8
+ cachedInputTokens: number;
9
+ cacheWriteInputTokens: number;
10
+ outputTokens: number;
11
+ };
12
+
13
+ export type AppServerTurnResult = {
14
+ status: string;
15
+ error?: string;
16
+ };
17
+
18
+ type NativeToolName = 'apply_patch' | 'view_image';
19
+
20
+ type NativeToolCall = {
21
+ name: NativeToolName;
22
+ resultEmitted: boolean;
23
+ successConfirmed: boolean;
24
+ };
25
+
26
+ export function createAppServerEventHandler({
27
+ stepTracker,
28
+ emitStreamEvent,
29
+ emitWarning,
30
+ emitError,
31
+ }: {
32
+ stepTracker: CodexStepTracker;
33
+ emitStreamEvent: (event: CodexEvent) => void;
34
+ emitWarning: BridgeTurn['emitWarning'];
35
+ emitError: BridgeTurn['emitError'];
36
+ }): {
37
+ announceThread(threadId: string): void;
38
+ handle(notification: CodexAppServerNotification): void;
39
+ setTurnId(turnId: string): void;
40
+ waitForCompletion(): Promise<AppServerTurnResult>;
41
+ } {
42
+ let activeThreadId: string | undefined;
43
+ let activeTurnId: string | undefined;
44
+ let settled = false;
45
+ let accumulatedUsage: UsageBreakdown = emptyUsageBreakdown();
46
+ let lastCumulativeUsageKey: string | undefined;
47
+ const textByItem = new Map<string, string>();
48
+ const reasoningByItem = new Map<string, string>();
49
+ const nativeToolCalls = new Map<string, NativeToolCall>();
50
+ let resolveCompletion: (result: AppServerTurnResult) => void = () => {};
51
+ const completion = new Promise<AppServerTurnResult>(resolve => {
52
+ resolveCompletion = resolve;
53
+ });
54
+ const announceThread = (threadId: string): void => {
55
+ if (activeThreadId != null) return;
56
+ activeThreadId = threadId;
57
+ emitStreamEvent({ type: 'thread.started', thread_id: threadId });
58
+ };
59
+
60
+ const handleItem = ({
61
+ eventType,
62
+ params,
63
+ }: {
64
+ eventType: 'item.started' | 'item.completed';
65
+ params: Record<string, unknown>;
66
+ }): void => {
67
+ if (!matchesActiveTurn({ params, activeThreadId, activeTurnId })) return;
68
+ const item = asRecord(params.item);
69
+ if (item == null || typeof item.type !== 'string') return;
70
+ if (eventType === 'item.completed' && typeof item.id === 'string') {
71
+ const call = nativeToolCalls.get(item.id);
72
+ if (call?.name === 'apply_patch' && item.type === 'fileChange') {
73
+ call.successConfirmed = item.status === 'completed';
74
+ } else if (call?.name === 'view_image' && item.type === 'imageView') {
75
+ call.successConfirmed = true;
76
+ }
77
+ }
78
+ const normalized = normalizeItem({ item, textByItem, reasoningByItem });
79
+ if (normalized == null) return;
80
+ if (normalized.type === 'dynamic_tool_call') {
81
+ stepTracker.observeEvent({
82
+ event: { type: eventType, item: normalized },
83
+ itemId: normalized.id,
84
+ });
85
+ return;
86
+ }
87
+ emitStreamEvent({ type: eventType, item: normalized });
88
+ };
89
+
90
+ const emitNativeToolResult = ({
91
+ callId,
92
+ result,
93
+ }: {
94
+ callId: string;
95
+ result: unknown;
96
+ }): void => {
97
+ const call = nativeToolCalls.get(callId);
98
+ if (call == null || call.resultEmitted) return;
99
+ call.resultEmitted = true;
100
+ emitStreamEvent({
101
+ type: 'item.completed',
102
+ item: {
103
+ type: 'native_tool',
104
+ id: callId,
105
+ tool: call.name,
106
+ result,
107
+ ...(!call.successConfirmed ? { isError: true } : {}),
108
+ },
109
+ });
110
+ };
111
+
112
+ const handleRawItem = (params: Record<string, unknown>): void => {
113
+ if (!matchesActiveTurn({ params, activeThreadId, activeTurnId })) return;
114
+ const item = asRecord(params.item);
115
+ if (item == null) return;
116
+ const toolCall = normalizeNativeToolCall({ item });
117
+ if (toolCall != null) {
118
+ if (nativeToolCalls.has(toolCall.callId)) return;
119
+ nativeToolCalls.set(toolCall.callId, {
120
+ name: toolCall.name,
121
+ resultEmitted: false,
122
+ successConfirmed: false,
123
+ });
124
+ emitStreamEvent({
125
+ type: 'item.started',
126
+ item: {
127
+ type: 'native_tool',
128
+ id: toolCall.callId,
129
+ tool: toolCall.name,
130
+ input: toolCall.input,
131
+ },
132
+ });
133
+ return;
134
+ }
135
+ if (typeof item.call_id !== 'string') return;
136
+ const call = nativeToolCalls.get(item.call_id);
137
+ if (
138
+ call == null ||
139
+ (call.name === 'apply_patch'
140
+ ? item.type !== 'custom_tool_call_output'
141
+ : item.type !== 'function_call_output') ||
142
+ !Object.prototype.hasOwnProperty.call(item, 'output')
143
+ ) {
144
+ return;
145
+ }
146
+ emitNativeToolResult({
147
+ callId: item.call_id,
148
+ result:
149
+ call.name === 'view_image' &&
150
+ call.successConfirmed &&
151
+ Array.isArray(item.output) &&
152
+ item.output.length === 0
153
+ ? 'Image viewed.'
154
+ : item.output,
155
+ });
156
+ };
157
+
158
+ return {
159
+ announceThread,
160
+ setTurnId(turnId) {
161
+ activeTurnId = turnId;
162
+ },
163
+ handle(notification) {
164
+ const params = asRecord(notification.params);
165
+ if (notification.method === 'thread/started') {
166
+ const thread = asRecord(params?.thread);
167
+ if (typeof thread?.id === 'string') announceThread(thread.id);
168
+ return;
169
+ }
170
+ if (notification.method === 'turn/started') {
171
+ const turn = asRecord(params?.turn);
172
+ if (
173
+ params?.threadId === activeThreadId &&
174
+ typeof turn?.id === 'string'
175
+ ) {
176
+ activeTurnId = turn.id;
177
+ }
178
+ return;
179
+ }
180
+ if (notification.method === 'item/started' && params != null) {
181
+ handleItem({ eventType: 'item.started', params });
182
+ return;
183
+ }
184
+ if (notification.method === 'item/completed' && params != null) {
185
+ handleItem({ eventType: 'item.completed', params });
186
+ return;
187
+ }
188
+ if (
189
+ notification.method === 'rawResponseItem/completed' &&
190
+ params != null
191
+ ) {
192
+ handleRawItem(params);
193
+ return;
194
+ }
195
+ if (
196
+ notification.method === 'item/agentMessage/delta' &&
197
+ params != null &&
198
+ matchesActiveTurn({ params, activeThreadId, activeTurnId }) &&
199
+ typeof params.itemId === 'string' &&
200
+ typeof params.delta === 'string'
201
+ ) {
202
+ const text = (textByItem.get(params.itemId) ?? '') + params.delta;
203
+ textByItem.set(params.itemId, text);
204
+ emitStreamEvent({
205
+ type: 'item.updated',
206
+ item: { type: 'agent_message', id: params.itemId, text },
207
+ });
208
+ return;
209
+ }
210
+ if (
211
+ notification.method === 'item/reasoning/summaryTextDelta' &&
212
+ params != null &&
213
+ matchesActiveTurn({ params, activeThreadId, activeTurnId }) &&
214
+ typeof params.itemId === 'string' &&
215
+ typeof params.delta === 'string'
216
+ ) {
217
+ const text = (reasoningByItem.get(params.itemId) ?? '') + params.delta;
218
+ reasoningByItem.set(params.itemId, text);
219
+ emitStreamEvent({
220
+ type: 'item.updated',
221
+ item: { type: 'reasoning', id: params.itemId, text },
222
+ });
223
+ return;
224
+ }
225
+ if (
226
+ notification.method === 'thread/tokenUsage/updated' &&
227
+ params != null &&
228
+ matchesActiveTurn({ params, activeThreadId, activeTurnId })
229
+ ) {
230
+ const tokenUsage = asRecord(params.tokenUsage);
231
+ const total = readUsageBreakdown(tokenUsage?.total);
232
+ const last = readUsageBreakdown(tokenUsage?.last);
233
+ if (total != null && last != null) {
234
+ const key = usageKey(total);
235
+ if (key !== lastCumulativeUsageKey) {
236
+ lastCumulativeUsageKey = key;
237
+ accumulatedUsage = addUsage({
238
+ total: accumulatedUsage,
239
+ increment: last,
240
+ });
241
+ }
242
+ }
243
+ return;
244
+ }
245
+ if (notification.method === 'error' && params != null) {
246
+ if (!matchesActiveTurn({ params, activeThreadId, activeTurnId }))
247
+ return;
248
+ const error = asRecord(params.error);
249
+ const message =
250
+ typeof error?.message === 'string'
251
+ ? error.message
252
+ : 'Codex app-server reported an error.';
253
+ if (params.willRetry === true) {
254
+ emitWarning({ message });
255
+ } else {
256
+ emitError({ error: message, message: 'codex turn failed' });
257
+ }
258
+ return;
259
+ }
260
+ if (
261
+ (notification.method === 'warning' ||
262
+ notification.method === 'configWarning' ||
263
+ notification.method === 'deprecationNotice') &&
264
+ typeof params?.message === 'string'
265
+ ) {
266
+ emitWarning({ message: params.message });
267
+ return;
268
+ }
269
+ if (
270
+ notification.method === 'turn/completed' &&
271
+ params != null &&
272
+ matchesActiveTurn({ params, activeThreadId, activeTurnId })
273
+ ) {
274
+ if (settled) return;
275
+ settled = true;
276
+ const turn = asRecord(params.turn);
277
+ const status =
278
+ typeof turn?.status === 'string' ? turn.status : 'failed';
279
+ const turnError = asRecord(turn?.error);
280
+ if (status === 'completed') {
281
+ for (const [callId, call] of nativeToolCalls) {
282
+ if (call.resultEmitted) continue;
283
+ emitNativeToolResult({
284
+ callId,
285
+ result: call.successConfirmed
286
+ ? call.name === 'apply_patch'
287
+ ? 'Patch applied.'
288
+ : 'Image viewed.'
289
+ : 'Codex did not report the tool result.',
290
+ });
291
+ }
292
+ }
293
+ emitStreamEvent({
294
+ type: 'turn.completed',
295
+ usage: toLegacyUsage(accumulatedUsage),
296
+ });
297
+ resolveCompletion({
298
+ status,
299
+ ...(typeof turnError?.message === 'string'
300
+ ? { error: turnError.message }
301
+ : {}),
302
+ });
303
+ }
304
+ },
305
+ waitForCompletion: () => completion,
306
+ };
307
+ }
308
+
309
+ function normalizeNativeToolCall({
310
+ item,
311
+ }: {
312
+ item: Record<string, unknown>;
313
+ }): { callId: string; name: NativeToolName; input: string } | undefined {
314
+ if (
315
+ item.namespace != null ||
316
+ typeof item.call_id !== 'string' ||
317
+ item.call_id.length === 0
318
+ ) {
319
+ return undefined;
320
+ }
321
+ if (
322
+ item.type === 'custom_tool_call' &&
323
+ item.name === 'apply_patch' &&
324
+ typeof item.input === 'string'
325
+ ) {
326
+ return {
327
+ callId: item.call_id,
328
+ name: 'apply_patch',
329
+ input: JSON.stringify(item.input),
330
+ };
331
+ }
332
+ if (
333
+ item.type === 'function_call' &&
334
+ item.name === 'view_image' &&
335
+ typeof item.arguments === 'string'
336
+ ) {
337
+ return {
338
+ callId: item.call_id,
339
+ name: 'view_image',
340
+ input: item.arguments,
341
+ };
342
+ }
343
+ return undefined;
344
+ }
345
+
346
+ function normalizeItem({
347
+ item,
348
+ textByItem,
349
+ reasoningByItem,
350
+ }: {
351
+ item: Record<string, unknown>;
352
+ textByItem: Map<string, string>;
353
+ reasoningByItem: Map<string, string>;
354
+ }): CodexItem | undefined {
355
+ const id = typeof item.id === 'string' ? item.id : undefined;
356
+ if (item.type === 'agentMessage') {
357
+ const text = typeof item.text === 'string' ? item.text : '';
358
+ if (id != null) textByItem.set(id, text);
359
+ return { type: 'agent_message', id, text };
360
+ }
361
+ if (item.type === 'reasoning') {
362
+ const summary = stringArray(item.summary).join('\n\n');
363
+ const content = stringArray(item.content).join('\n\n');
364
+ const text = summary || reasoningByItem.get(id ?? '') || content;
365
+ if (id != null) reasoningByItem.set(id, text);
366
+ return { type: 'reasoning', id, text };
367
+ }
368
+ if (item.type === 'commandExecution') {
369
+ return {
370
+ type: 'command_execution',
371
+ id,
372
+ command: typeof item.command === 'string' ? item.command : '',
373
+ exit_code: typeof item.exitCode === 'number' ? item.exitCode : undefined,
374
+ aggregated_output:
375
+ typeof item.aggregatedOutput === 'string'
376
+ ? item.aggregatedOutput
377
+ : undefined,
378
+ status: normalizeCommandStatus(item.status),
379
+ };
380
+ }
381
+ if (item.type === 'mcpToolCall') {
382
+ const result = asRecord(item.result);
383
+ return {
384
+ type: 'mcp_tool_call',
385
+ id,
386
+ server: typeof item.server === 'string' ? item.server : undefined,
387
+ tool: typeof item.tool === 'string' ? item.tool : undefined,
388
+ arguments: item.arguments,
389
+ result:
390
+ result == null
391
+ ? item.result
392
+ : {
393
+ content: result.content,
394
+ structured_content: result.structuredContent,
395
+ },
396
+ error: asRecord(item.error) as { message?: string } | undefined,
397
+ };
398
+ }
399
+ if (item.type === 'dynamicToolCall') {
400
+ return { type: 'dynamic_tool_call', id };
401
+ }
402
+ if (item.type === 'webSearch') {
403
+ return {
404
+ type: 'web_search',
405
+ id,
406
+ query: typeof item.query === 'string' ? item.query : undefined,
407
+ action: asRecord(item.action) ?? undefined,
408
+ result: item.results,
409
+ };
410
+ }
411
+ if (item.type === 'fileChange') {
412
+ return {
413
+ type: 'file_change',
414
+ id,
415
+ changes: Array.isArray(item.changes)
416
+ ? item.changes.flatMap(change => {
417
+ const value = asRecord(change);
418
+ const kind = asRecord(value?.kind)?.type;
419
+ return typeof value?.path === 'string' &&
420
+ (kind === 'add' || kind === 'delete' || kind === 'update')
421
+ ? [{ path: value.path, kind }]
422
+ : [];
423
+ })
424
+ : [],
425
+ };
426
+ }
427
+ if (item.type === 'plan') return { type: 'todo_list', id };
428
+ return undefined;
429
+ }
430
+
431
+ function normalizeCommandStatus(
432
+ value: unknown,
433
+ ): 'in_progress' | 'completed' | 'failed' {
434
+ if (value === 'completed') return 'completed';
435
+ if (value === 'inProgress') return 'in_progress';
436
+ return 'failed';
437
+ }
438
+
439
+ function matchesActiveTurn({
440
+ params,
441
+ activeThreadId,
442
+ activeTurnId,
443
+ }: {
444
+ params: Record<string, unknown>;
445
+ activeThreadId: string | undefined;
446
+ activeTurnId: string | undefined;
447
+ }): boolean {
448
+ const nestedTurn = asRecord(params.turn);
449
+ const turnId =
450
+ typeof params.turnId === 'string'
451
+ ? params.turnId
452
+ : typeof nestedTurn?.id === 'string'
453
+ ? nestedTurn.id
454
+ : undefined;
455
+ return (
456
+ activeThreadId != null &&
457
+ activeTurnId != null &&
458
+ params.threadId === activeThreadId &&
459
+ turnId === activeTurnId
460
+ );
461
+ }
462
+
463
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
464
+ return value != null && typeof value === 'object' && !Array.isArray(value)
465
+ ? (value as Record<string, unknown>)
466
+ : undefined;
467
+ }
468
+
469
+ function stringArray(value: unknown): string[] {
470
+ return Array.isArray(value)
471
+ ? value.filter((entry): entry is string => typeof entry === 'string')
472
+ : [];
473
+ }
474
+
475
+ function emptyUsageBreakdown(): UsageBreakdown {
476
+ return {
477
+ inputTokens: 0,
478
+ cachedInputTokens: 0,
479
+ cacheWriteInputTokens: 0,
480
+ outputTokens: 0,
481
+ };
482
+ }
483
+
484
+ function readUsageBreakdown(value: unknown): UsageBreakdown | undefined {
485
+ const usage = asRecord(value);
486
+ if (usage == null) return undefined;
487
+ return {
488
+ inputTokens: numberOrZero(usage.inputTokens),
489
+ cachedInputTokens: numberOrZero(usage.cachedInputTokens),
490
+ cacheWriteInputTokens: numberOrZero(usage.cacheWriteInputTokens),
491
+ outputTokens: numberOrZero(usage.outputTokens),
492
+ };
493
+ }
494
+
495
+ function addUsage({
496
+ total,
497
+ increment,
498
+ }: {
499
+ total: UsageBreakdown;
500
+ increment: UsageBreakdown;
501
+ }): UsageBreakdown {
502
+ return {
503
+ inputTokens: total.inputTokens + increment.inputTokens,
504
+ cachedInputTokens: total.cachedInputTokens + increment.cachedInputTokens,
505
+ cacheWriteInputTokens:
506
+ total.cacheWriteInputTokens + increment.cacheWriteInputTokens,
507
+ outputTokens: total.outputTokens + increment.outputTokens,
508
+ };
509
+ }
510
+
511
+ function usageKey(usage: UsageBreakdown): string {
512
+ return [
513
+ usage.inputTokens,
514
+ usage.cachedInputTokens,
515
+ usage.cacheWriteInputTokens,
516
+ usage.outputTokens,
517
+ ].join(':');
518
+ }
519
+
520
+ function toLegacyUsage(usage: UsageBreakdown): Record<string, number> {
521
+ return {
522
+ input_tokens: usage.inputTokens,
523
+ cached_input_tokens: usage.cachedInputTokens,
524
+ cache_write_input_tokens: usage.cacheWriteInputTokens,
525
+ output_tokens: usage.outputTokens,
526
+ };
527
+ }
528
+
529
+ function numberOrZero(value: unknown): number {
530
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
531
+ }
@@ -15,6 +15,8 @@ export type CodexItem = {
15
15
  status?: 'in_progress' | 'completed' | 'failed';
16
16
  server?: string;
17
17
  tool?: string;
18
+ input?: string;
19
+ isError?: boolean;
18
20
  arguments?: unknown;
19
21
  result?: { content?: unknown; structured_content?: unknown } | unknown;
20
22
  error?: { message?: string };
@@ -114,6 +116,28 @@ export function createEmitStreamEvent({
114
116
  stepTracker.observeEvent({ event, itemId: id });
115
117
  };
116
118
 
119
+ if (item.type === 'native_tool' && item.tool != null) {
120
+ if (event.type === 'item.started' && item.input != null) {
121
+ send({
122
+ type: 'tool-call',
123
+ toolCallId: id,
124
+ toolName: item.tool,
125
+ input: item.input,
126
+ providerExecuted: true,
127
+ });
128
+ } else if (event.type === 'item.completed') {
129
+ send({
130
+ type: 'tool-result',
131
+ toolCallId: id,
132
+ toolName: item.tool,
133
+ result: item.result,
134
+ ...(item.isError ? { isError: true } : {}),
135
+ });
136
+ }
137
+ stepTracker.observeEvent({ event, itemId: `native-tool:${id}` });
138
+ return;
139
+ }
140
+
117
141
  if (item.type === 'agent_message' && typeof item.text === 'string') {
118
142
  /*
119
143
  * The presence of `id` in `textByItem` — not the `item.started` event —
@@ -323,12 +347,13 @@ function extractMcpToolCallResult(item: CodexItem): unknown {
323
347
  function mapUsage(usage: Record<string, number>): Record<string, unknown> {
324
348
  const input = usage.input_tokens ?? 0;
325
349
  const cacheRead = usage.cached_input_tokens ?? 0;
350
+ const cacheWrite = usage.cache_write_input_tokens ?? 0;
326
351
  return {
327
352
  inputTokens: {
328
353
  total: input,
329
- noCache: Math.max(0, input - cacheRead),
354
+ noCache: Math.max(0, input - cacheRead - cacheWrite),
330
355
  cacheRead,
331
- cacheWrite: 0,
356
+ cacheWrite,
332
357
  },
333
358
  outputTokens: {
334
359
  total: usage.output_tokens ?? 0,