pi-openai-codex-compat 0.0.7 → 0.0.8
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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.8 - 2026-08-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Match Codex tool-call commit semantics by trusting only `response.output_item.done`, ignoring terminal output snapshots and terminal-only calls, executing the completed subset of mixed batches, and preserving completed calls across incomplete or failed response handling.
|
|
10
|
+
|
|
5
11
|
## 0.0.7 - 2026-08-16
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -115,6 +115,7 @@ type ActiveAgentTurn = {
|
|
|
115
115
|
turnId: string;
|
|
116
116
|
startedAtUnixMs: number;
|
|
117
117
|
turnState: CodexTurnState;
|
|
118
|
+
pendingPostToolDisposition?: CodexPostToolDisposition;
|
|
118
119
|
};
|
|
119
120
|
|
|
120
121
|
type CodexCompat = {
|
|
@@ -130,25 +131,27 @@ type CodexTerminalState = {
|
|
|
130
131
|
|
|
131
132
|
type CodexAttemptCapture = {
|
|
132
133
|
streamedItems: ResponsesItem[];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
terminalItems?: ResponsesItem[];
|
|
134
|
+
streamedToolCallIndexes: Set<number>;
|
|
135
|
+
streamedCompletedToolCallIndexes: Set<number>;
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
type CodexToolCallAssessment = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
completedCount: number;
|
|
140
|
+
discardedPartialCount: number;
|
|
141
|
+
hasCompletedCalls: boolean;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
type CodexPostToolDisposition = {
|
|
145
|
+
callIds: string[];
|
|
146
|
+
response?: JsonRecord;
|
|
147
|
+
retryAttempt: number;
|
|
148
|
+
terminalType: "response.incomplete" | "response.failed";
|
|
149
|
+
type: "error" | "retry";
|
|
144
150
|
};
|
|
145
151
|
|
|
146
152
|
type CodexResponseDecision =
|
|
147
153
|
| "continue_no_tools"
|
|
148
|
-
| "preserve_terminal_error"
|
|
149
|
-
| "reject_terminal_stream_mismatch"
|
|
150
154
|
| "retry_original_input"
|
|
151
|
-
| "return_length_incomplete_call"
|
|
152
155
|
| "return_terminal"
|
|
153
156
|
| "return_tool_use";
|
|
154
157
|
|
|
@@ -217,62 +220,22 @@ function isToolCallItem(item: ResponsesItem): boolean {
|
|
|
217
220
|
return item.type === "function_call" || item.type === "custom_tool_call";
|
|
218
221
|
}
|
|
219
222
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
const itemId = typeof item.id === "string" ? item.id : undefined;
|
|
223
|
-
return `${String(item.type)}:${itemId ? `item:${itemId}` : `call:${callId ?? "missing"}`}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function hasValidToolCallPayload(item: ResponsesItem): boolean {
|
|
227
|
-
if (
|
|
228
|
-
typeof item.id !== "string" ||
|
|
229
|
-
typeof item["call_id"] !== "string" ||
|
|
230
|
-
typeof item.name !== "string"
|
|
231
|
-
) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
if (item.type === "custom_tool_call") return typeof item.input === "string";
|
|
235
|
-
if (item.type !== "function_call" || typeof item.arguments !== "string") return false;
|
|
236
|
-
try {
|
|
237
|
-
return isObject(JSON.parse(item.arguments));
|
|
238
|
-
} catch {
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function toolCallIsComplete(
|
|
244
|
-
item: ResponsesItem,
|
|
245
|
-
capture: CodexAttemptCapture,
|
|
246
|
-
terminalType: CodexTerminalState["type"],
|
|
247
|
-
): boolean {
|
|
248
|
-
if (!hasValidToolCallPayload(item)) return false;
|
|
249
|
-
const status = typeof item["status"] === "string" ? item["status"] : undefined;
|
|
250
|
-
if (status !== undefined) return status === "completed";
|
|
251
|
-
if (capture.streamedCompletedToolCallKeys.has(toolCallKey(item))) return true;
|
|
252
|
-
return terminalType === "response.completed";
|
|
223
|
+
function eventOutputIndex(event: JsonRecord): number {
|
|
224
|
+
return typeof event["output_index"] === "number" ? event["output_index"] : 0;
|
|
253
225
|
}
|
|
254
226
|
|
|
255
227
|
function assessAttemptToolCalls(
|
|
256
228
|
items: readonly ResponsesItem[],
|
|
257
229
|
capture: CodexAttemptCapture,
|
|
258
|
-
terminalType: CodexTerminalState["type"],
|
|
259
230
|
): CodexToolCallAssessment {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
(key) => !authoritativeKeys.has(key),
|
|
231
|
+
const completedCount = items.filter(isToolCallItem).length;
|
|
232
|
+
const discardedPartialCount = [...capture.streamedToolCallIndexes].filter(
|
|
233
|
+
(index) => !capture.streamedCompletedToolCallIndexes.has(index),
|
|
264
234
|
).length;
|
|
265
|
-
const hasToolCalls = calls.length > 0 || capture.streamedToolCallKeys.size > 0;
|
|
266
235
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
omittedStreamedCount === 0 &&
|
|
271
|
-
calls.length > 0 &&
|
|
272
|
-
calls.every((item) => toolCallIsComplete(item, capture, terminalType)),
|
|
273
|
-
authoritativeCount: calls.length,
|
|
274
|
-
omittedStreamedCount,
|
|
275
|
-
terminalCount: capture.terminalItems?.filter(isToolCallItem).length ?? 0,
|
|
236
|
+
completedCount,
|
|
237
|
+
discardedPartialCount,
|
|
238
|
+
hasCompletedCalls: completedCount > 0,
|
|
276
239
|
};
|
|
277
240
|
}
|
|
278
241
|
|
|
@@ -291,6 +254,7 @@ function responseDecisionDiagnostic(options: {
|
|
|
291
254
|
capture: CodexAttemptCapture;
|
|
292
255
|
decision: CodexResponseDecision;
|
|
293
256
|
incompleteReason?: string;
|
|
257
|
+
postToolDisposition?: "continue" | "error" | "retry";
|
|
294
258
|
terminalState: CodexTerminalState;
|
|
295
259
|
toolCalls: CodexToolCallAssessment;
|
|
296
260
|
}): AssistantMessageDiagnostic | undefined {
|
|
@@ -302,8 +266,7 @@ function responseDecisionDiagnostic(options: {
|
|
|
302
266
|
options.attempt > 1 ||
|
|
303
267
|
options.terminalState.type !== "response.completed" ||
|
|
304
268
|
endTurn === false ||
|
|
305
|
-
options.
|
|
306
|
-
options.toolCalls.omittedStreamedCount > 0;
|
|
269
|
+
options.capture.streamedToolCallIndexes.size > 0;
|
|
307
270
|
if (!nontrivial) return undefined;
|
|
308
271
|
|
|
309
272
|
return {
|
|
@@ -314,14 +277,12 @@ function responseDecisionDiagnostic(options: {
|
|
|
314
277
|
terminalType: options.terminalState.type ?? "missing",
|
|
315
278
|
...(options.incompleteReason ? { incompleteReason: options.incompleteReason } : {}),
|
|
316
279
|
...(endTurn === undefined ? {} : { endTurn }),
|
|
317
|
-
itemSource: options.capture.terminalItems ? "terminal" : "stream-fallback",
|
|
318
280
|
outputItemTypes: outputItemTypeCounts(options.attemptItems),
|
|
319
|
-
streamedCallsStarted: options.capture.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
allCallsComplete: options.toolCalls.allComplete,
|
|
281
|
+
streamedCallsStarted: options.capture.streamedToolCallIndexes.size,
|
|
282
|
+
streamedCallsDone: options.capture.streamedCompletedToolCallIndexes.size,
|
|
283
|
+
returnedCalls: options.toolCalls.completedCount,
|
|
284
|
+
discardedPartialCalls: options.toolCalls.discardedPartialCount,
|
|
285
|
+
...(options.postToolDisposition ? { postToolDisposition: options.postToolDisposition } : {}),
|
|
325
286
|
decision: options.decision,
|
|
326
287
|
},
|
|
327
288
|
};
|
|
@@ -340,28 +301,17 @@ function captureRawEvents(
|
|
|
340
301
|
isResponsesItem(event.item) &&
|
|
341
302
|
isToolCallItem(event.item)
|
|
342
303
|
) {
|
|
343
|
-
capture.
|
|
304
|
+
capture.streamedToolCallIndexes.add(eventOutputIndex(event));
|
|
344
305
|
}
|
|
345
306
|
if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
|
|
346
307
|
const item = structuredClone(event.item);
|
|
347
308
|
capture.streamedItems.push(item);
|
|
348
309
|
if (isToolCallItem(item)) {
|
|
349
|
-
const
|
|
350
|
-
capture.
|
|
351
|
-
capture.
|
|
310
|
+
const index = eventOutputIndex(event);
|
|
311
|
+
capture.streamedToolCallIndexes.add(index);
|
|
312
|
+
capture.streamedCompletedToolCallIndexes.add(index);
|
|
352
313
|
}
|
|
353
314
|
}
|
|
354
|
-
if (
|
|
355
|
-
(event.type === "response.completed" ||
|
|
356
|
-
event.type === "response.incomplete" ||
|
|
357
|
-
event.type === "response.failed") &&
|
|
358
|
-
isObject(event.response) &&
|
|
359
|
-
Array.isArray(event.response["output"])
|
|
360
|
-
) {
|
|
361
|
-
capture.terminalItems = event.response["output"]
|
|
362
|
-
.filter(isResponsesItem)
|
|
363
|
-
.map((item) => structuredClone(item));
|
|
364
|
-
}
|
|
365
315
|
if (
|
|
366
316
|
terminalState &&
|
|
367
317
|
(event.type === "response.completed" ||
|
|
@@ -462,6 +412,48 @@ function retryableResponseFailure(response: JsonRecord | undefined): boolean {
|
|
|
462
412
|
);
|
|
463
413
|
}
|
|
464
414
|
|
|
415
|
+
function terminalErrorMessage(disposition: CodexPostToolDisposition): string {
|
|
416
|
+
if (disposition.terminalType === "response.failed") {
|
|
417
|
+
const error = isObject(disposition.response?.["error"])
|
|
418
|
+
? disposition.response["error"]
|
|
419
|
+
: undefined;
|
|
420
|
+
return typeof error?.["message"] === "string" ? error["message"] : "Codex response failed";
|
|
421
|
+
}
|
|
422
|
+
const details = isObject(disposition.response?.["incomplete_details"])
|
|
423
|
+
? disposition.response["incomplete_details"]
|
|
424
|
+
: undefined;
|
|
425
|
+
const reason = typeof details?.["reason"] === "string" ? details["reason"] : undefined;
|
|
426
|
+
return reason
|
|
427
|
+
? `Response incomplete: ${reason}`
|
|
428
|
+
: "Response incomplete without a provider reason";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function completedAttemptToolCallIds(
|
|
432
|
+
message: AssistantMessage,
|
|
433
|
+
attempt: CodexStreamAttemptState,
|
|
434
|
+
): string[] {
|
|
435
|
+
return [...attempt.completedContentIndexes]
|
|
436
|
+
.sort((left, right) => left - right)
|
|
437
|
+
.flatMap((index) => {
|
|
438
|
+
const block = message.content[index];
|
|
439
|
+
return block?.type === "toolCall" ? [block.id] : [];
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function assertLinkedToolOutputs(context: Context, disposition: CodexPostToolDisposition): void {
|
|
444
|
+
const outputIds = new Set(
|
|
445
|
+
context.messages.flatMap((message) =>
|
|
446
|
+
message.role === "toolResult" ? [message.toolCallId] : [],
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
const missing = disposition.callIds.filter((callId) => !outputIds.has(callId));
|
|
450
|
+
if (missing.length > 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Codex cannot process the ${disposition.terminalType} response until Pi records tool output for: ${missing.join(", ")}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
465
457
|
function responseRetryDelayMs(baseDelayMs: number, attempt: number): number {
|
|
466
458
|
if (baseDelayMs <= 0) return 0;
|
|
467
459
|
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
@@ -1169,6 +1161,26 @@ export class CodexProviderRuntime {
|
|
|
1169
1161
|
requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
|
|
1170
1162
|
const agentTurn = this.agentTurn(runtimeSessionId);
|
|
1171
1163
|
const responsesLiteEnabled = this.responsesLiteEnabled(runtimeSessionId);
|
|
1164
|
+
let carriedResponseRetries = 0;
|
|
1165
|
+
const pendingPostToolDisposition = agentTurn.pendingPostToolDisposition;
|
|
1166
|
+
if (pendingPostToolDisposition) {
|
|
1167
|
+
assertLinkedToolOutputs(context, pendingPostToolDisposition);
|
|
1168
|
+
delete agentTurn.pendingPostToolDisposition;
|
|
1169
|
+
if (
|
|
1170
|
+
pendingPostToolDisposition.type === "error" ||
|
|
1171
|
+
pendingPostToolDisposition.retryAttempt > this.responseRetryPolicy.maxRetries
|
|
1172
|
+
) {
|
|
1173
|
+
throw new Error(terminalErrorMessage(pendingPostToolDisposition));
|
|
1174
|
+
}
|
|
1175
|
+
carriedResponseRetries = pendingPostToolDisposition.retryAttempt;
|
|
1176
|
+
await waitForResponseRetry(
|
|
1177
|
+
responseRetryDelayMs(
|
|
1178
|
+
this.responseRetryPolicy.baseDelayMs,
|
|
1179
|
+
pendingPostToolDisposition.retryAttempt,
|
|
1180
|
+
),
|
|
1181
|
+
requestOptions.signal,
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1172
1184
|
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
1173
1185
|
context.tools,
|
|
1174
1186
|
(model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
|
|
@@ -1255,20 +1267,19 @@ export class CodexProviderRuntime {
|
|
|
1255
1267
|
},
|
|
1256
1268
|
};
|
|
1257
1269
|
let responseRequests = 0;
|
|
1258
|
-
let responseRetries =
|
|
1270
|
+
let responseRetries = carriedResponseRetries;
|
|
1259
1271
|
while (true) {
|
|
1260
1272
|
responseRequests += 1;
|
|
1261
1273
|
const attemptCapture: CodexAttemptCapture = {
|
|
1262
1274
|
streamedItems: [],
|
|
1263
|
-
|
|
1264
|
-
|
|
1275
|
+
streamedToolCallIndexes: new Set(),
|
|
1276
|
+
streamedCompletedToolCallIndexes: new Set(),
|
|
1265
1277
|
};
|
|
1266
1278
|
const terminalState: CodexTerminalState = {};
|
|
1267
1279
|
const attemptState: CodexStreamAttemptState = {
|
|
1268
1280
|
startedContentIndexes: new Set(),
|
|
1269
1281
|
completedContentIndexes: new Set(),
|
|
1270
1282
|
};
|
|
1271
|
-
const contentLengthBeforeAttempt = output.content.length;
|
|
1272
1283
|
const usageBeforeAttempt = structuredClone(output.usage);
|
|
1273
1284
|
output.usage = emptyUsage();
|
|
1274
1285
|
try {
|
|
@@ -1300,14 +1311,10 @@ export class CodexProviderRuntime {
|
|
|
1300
1311
|
throw error;
|
|
1301
1312
|
}
|
|
1302
1313
|
output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
const attemptItems = attemptCapture.
|
|
1306
|
-
const toolCalls = assessAttemptToolCalls(
|
|
1307
|
-
attemptItems,
|
|
1308
|
-
attemptCapture,
|
|
1309
|
-
terminalState.type,
|
|
1310
|
-
);
|
|
1314
|
+
// `response.output_item.done` is Codex's item-level commit point. Terminal
|
|
1315
|
+
// response.output snapshots are deliberately ignored.
|
|
1316
|
+
const attemptItems = attemptCapture.streamedItems;
|
|
1317
|
+
const toolCalls = assessAttemptToolCalls(attemptItems, attemptCapture);
|
|
1311
1318
|
const incompleteDetails = isObject(terminalState.response?.["incomplete_details"])
|
|
1312
1319
|
? terminalState.response["incomplete_details"]
|
|
1313
1320
|
: undefined;
|
|
@@ -1315,67 +1322,55 @@ export class CodexProviderRuntime {
|
|
|
1315
1322
|
typeof incompleteDetails?.["reason"] === "string"
|
|
1316
1323
|
? incompleteDetails["reason"]
|
|
1317
1324
|
: undefined;
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1325
|
+
const recordDecision = (
|
|
1326
|
+
decision: CodexResponseDecision,
|
|
1327
|
+
postToolDisposition?: "continue" | "error" | "retry",
|
|
1328
|
+
): void => {
|
|
1322
1329
|
const diagnostic = responseDecisionDiagnostic({
|
|
1323
1330
|
attempt: responseRequests,
|
|
1324
1331
|
attemptItems,
|
|
1325
1332
|
capture: attemptCapture,
|
|
1326
1333
|
decision,
|
|
1327
1334
|
...(incompleteReason ? { incompleteReason } : {}),
|
|
1335
|
+
...(postToolDisposition ? { postToolDisposition } : {}),
|
|
1328
1336
|
terminalState,
|
|
1329
1337
|
toolCalls,
|
|
1330
1338
|
});
|
|
1331
1339
|
if (diagnostic) output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
|
|
1332
1340
|
};
|
|
1333
1341
|
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
) {
|
|
1342
|
-
discardIncompleteAttemptContent(output, attemptState);
|
|
1343
|
-
rawItems.push(...attemptItems.map((item) => structuredClone(item)));
|
|
1344
|
-
output.stopReason = "toolUse";
|
|
1345
|
-
delete output.errorMessage;
|
|
1346
|
-
recordDecision("return_tool_use");
|
|
1347
|
-
break;
|
|
1342
|
+
// Return each done tool call before processing an unsuccessful response
|
|
1343
|
+
// terminal. Pi will execute the completed subset; started-only siblings
|
|
1344
|
+
// are discarded and never enter provider history.
|
|
1345
|
+
if (toolCalls.hasCompletedCalls) {
|
|
1346
|
+
const callIds = completedAttemptToolCallIds(output, attemptState);
|
|
1347
|
+
if (callIds.length !== toolCalls.completedCount) {
|
|
1348
|
+
throw new Error("Codex completed tool-call items could not be mapped to Pi calls.");
|
|
1348
1349
|
}
|
|
1349
|
-
|
|
1350
|
-
output.content.splice(contentLengthBeforeAttempt);
|
|
1350
|
+
let postToolDisposition: "continue" | "error" | "retry" = "continue";
|
|
1351
1351
|
if (
|
|
1352
|
-
terminalState.type === "response.
|
|
1353
|
-
|
|
1354
|
-
responseRetries < this.responseRetryPolicy.maxRetries
|
|
1352
|
+
terminalState.type === "response.incomplete" ||
|
|
1353
|
+
terminalState.type === "response.failed"
|
|
1355
1354
|
) {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
output.rawStopReason = "incomplete.tool_call";
|
|
1370
|
-
delete output.errorMessage;
|
|
1355
|
+
const retryable =
|
|
1356
|
+
terminalState.type === "response.incomplete" ||
|
|
1357
|
+
retryableResponseFailure(terminalState.response);
|
|
1358
|
+
postToolDisposition = retryable ? "retry" : "error";
|
|
1359
|
+
agentTurn.pendingPostToolDisposition = {
|
|
1360
|
+
callIds,
|
|
1361
|
+
...(terminalState.response
|
|
1362
|
+
? { response: structuredClone(terminalState.response) }
|
|
1363
|
+
: {}),
|
|
1364
|
+
retryAttempt: retryable ? responseRetries + 1 : responseRetries,
|
|
1365
|
+
terminalType: terminalState.type,
|
|
1366
|
+
type: postToolDisposition,
|
|
1367
|
+
};
|
|
1371
1368
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
: "preserve_terminal_error",
|
|
1378
|
-
);
|
|
1369
|
+
discardIncompleteAttemptContent(output, attemptState);
|
|
1370
|
+
rawItems.push(...attemptItems.map((item) => structuredClone(item)));
|
|
1371
|
+
output.stopReason = "toolUse";
|
|
1372
|
+
delete output.errorMessage;
|
|
1373
|
+
recordDecision("return_tool_use", postToolDisposition);
|
|
1379
1374
|
break;
|
|
1380
1375
|
}
|
|
1381
1376
|
|
|
@@ -1396,7 +1391,9 @@ export class CodexProviderRuntime {
|
|
|
1396
1391
|
output.stopReason = "pending";
|
|
1397
1392
|
delete output.errorMessage;
|
|
1398
1393
|
delete output.rawStopReason;
|
|
1399
|
-
recordDecision(
|
|
1394
|
+
recordDecision(
|
|
1395
|
+
attemptItems.length === 0 ? "retry_original_input" : "continue_no_tools",
|
|
1396
|
+
);
|
|
1400
1397
|
await waitForResponseRetry(
|
|
1401
1398
|
responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
|
|
1402
1399
|
requestOptions.signal,
|
|
@@ -141,11 +141,6 @@ function appendCustomInput(
|
|
|
141
141
|
return delta;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function responseItems(value: unknown): JsonRecord[] {
|
|
145
|
-
if (!Array.isArray(value)) return [];
|
|
146
|
-
return value.filter(isObject);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
144
|
function itemContentText(item: JsonRecord): string {
|
|
150
145
|
if (!Array.isArray(item.content)) return "";
|
|
151
146
|
return item.content
|
|
@@ -210,7 +205,7 @@ export async function processCodexStream(
|
|
|
210
205
|
let terminal = false;
|
|
211
206
|
const slots = new Map<number, OutputSlot>();
|
|
212
207
|
const completedOutputItems = new Set<string>();
|
|
213
|
-
const
|
|
208
|
+
const completedToolCallContentIndexes = new Set<number>();
|
|
214
209
|
const applyMessagePhaseStopReason = (item: JsonRecord): void => {
|
|
215
210
|
if (item.type === "message" && item["phase"] === "final_answer") {
|
|
216
211
|
output.stopReason = "stop";
|
|
@@ -336,7 +331,6 @@ export async function processCodexStream(
|
|
|
336
331
|
if (item.type === "reasoning" && slot?.type === "thinking") {
|
|
337
332
|
slot.block.thinking = reasoningText(item) || slot.block.thinking;
|
|
338
333
|
slot.block.thinkingSignature = JSON.stringify(item);
|
|
339
|
-
if (typeof item.id === "string") reasoningById.set(item.id, slot.block);
|
|
340
334
|
stream.push({
|
|
341
335
|
type: "thinking_end",
|
|
342
336
|
contentIndex: slot.contentIndex,
|
|
@@ -375,6 +369,7 @@ export async function processCodexStream(
|
|
|
375
369
|
partial: output,
|
|
376
370
|
});
|
|
377
371
|
trackCompleted(slot);
|
|
372
|
+
completedToolCallContentIndexes.add(slot.contentIndex);
|
|
378
373
|
slots.delete(index);
|
|
379
374
|
} else if (item.type === "custom_tool_call" && slot?.type === "toolCall") {
|
|
380
375
|
slot.block.name = piToolCallName(item);
|
|
@@ -388,6 +383,7 @@ export async function processCodexStream(
|
|
|
388
383
|
partial: output,
|
|
389
384
|
});
|
|
390
385
|
trackCompleted(slot);
|
|
386
|
+
completedToolCallContentIndexes.add(slot.contentIndex);
|
|
391
387
|
slots.delete(index);
|
|
392
388
|
}
|
|
393
389
|
};
|
|
@@ -424,20 +420,6 @@ export async function processCodexStream(
|
|
|
424
420
|
output.usage,
|
|
425
421
|
typeof response.service_tier === "string" ? response.service_tier : undefined,
|
|
426
422
|
);
|
|
427
|
-
const terminalItems = responseItems(response["output"]);
|
|
428
|
-
terminalItems.forEach((item, index) => completeOutputItem(index, item));
|
|
429
|
-
for (const item of terminalItems) {
|
|
430
|
-
if (item.type !== "reasoning" || typeof item.id !== "string") continue;
|
|
431
|
-
const block = reasoningById.get(item.id);
|
|
432
|
-
if (!block?.thinkingSignature || typeof item.encrypted_content !== "string") continue;
|
|
433
|
-
const stored = JSON.parse(block.thinkingSignature) as JsonRecord;
|
|
434
|
-
if (typeof stored.encrypted_content !== "string") {
|
|
435
|
-
block.thinkingSignature = JSON.stringify({
|
|
436
|
-
...stored,
|
|
437
|
-
encrypted_content: item.encrypted_content,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
423
|
const status = normalizeCodexStatus(response["status"]);
|
|
442
424
|
const incompleteDetails = isObject(response["incomplete_details"])
|
|
443
425
|
? response["incomplete_details"]
|
|
@@ -452,7 +434,12 @@ export async function processCodexStream(
|
|
|
452
434
|
output.stopReason = mappedStop.stopReason;
|
|
453
435
|
if (mappedStop.errorMessage === undefined) delete output.errorMessage;
|
|
454
436
|
else output.errorMessage = mappedStop.errorMessage;
|
|
455
|
-
if (
|
|
437
|
+
if (
|
|
438
|
+
output.stopReason === "stop" &&
|
|
439
|
+
[...completedToolCallContentIndexes].some(
|
|
440
|
+
(contentIndex) => output.content[contentIndex]?.type === "toolCall",
|
|
441
|
+
)
|
|
442
|
+
) {
|
|
456
443
|
output.stopReason = "toolUse";
|
|
457
444
|
}
|
|
458
445
|
};
|
|
@@ -1910,16 +1910,6 @@ async function* requestWebSocket(
|
|
|
1910
1910
|
responseCompleted = true;
|
|
1911
1911
|
}
|
|
1912
1912
|
if (typeof event.response.id === "string") responseId = event.response.id;
|
|
1913
|
-
if (Array.isArray(event.response["output"])) {
|
|
1914
|
-
const terminalItems = event.response["output"].filter(isObject);
|
|
1915
|
-
if (terminalItems.length > 0) {
|
|
1916
|
-
responseItems.splice(
|
|
1917
|
-
0,
|
|
1918
|
-
responseItems.length,
|
|
1919
|
-
...terminalItems.map((item) => structuredClone(item)),
|
|
1920
|
-
);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
1913
|
}
|
|
1924
1914
|
const normalized = normalizeEvent(event);
|
|
1925
1915
|
if (!normalized) continue;
|