pi-openai-codex-compat 0.0.6 → 0.0.7

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.
@@ -40,14 +40,79 @@ type ApplyPatchResult = {
40
40
  details?: unknown;
41
41
  };
42
42
 
43
+ export type ApplyPatchDebugResolver = () => boolean;
44
+
45
+ class ApplyPatchTitleComponent implements Component {
46
+ private readonly text = new Text("", 0, 0);
47
+ private readonly theme: Theme;
48
+ private readonly resolveDebug: ApplyPatchDebugResolver;
49
+
50
+ constructor(theme: Theme, resolveDebug: ApplyPatchDebugResolver) {
51
+ this.theme = theme;
52
+ this.resolveDebug = resolveDebug;
53
+ }
54
+
55
+ render(width: number): string[] {
56
+ const title = this.resolveDebug() ? "apply_patch (debug)" : "apply_patch";
57
+ this.text.setText(this.theme.fg("toolTitle", this.theme.bold(title)));
58
+ return this.text.render(width);
59
+ }
60
+
61
+ invalidate(): void {
62
+ this.text.invalidate();
63
+ }
64
+ }
65
+
66
+ function modelFeedback(result: ApplyPatchResult): string | undefined {
67
+ const text = result.content.flatMap((item) =>
68
+ item.type === "text" && typeof item.text === "string" ? [item.text] : [],
69
+ );
70
+ return text.length > 0 ? text.join("\n") : undefined;
71
+ }
72
+
73
+ class ApplyPatchResultComponent implements Component {
74
+ private readonly ordinary: Component;
75
+ private readonly feedback: Container | undefined;
76
+ private readonly expanded: boolean;
77
+ private readonly resolveDebug: ApplyPatchDebugResolver;
78
+
79
+ constructor(
80
+ ordinary: Component,
81
+ result: ApplyPatchResult,
82
+ expanded: boolean,
83
+ resolveDebug: ApplyPatchDebugResolver,
84
+ ) {
85
+ this.ordinary = ordinary;
86
+ this.expanded = expanded;
87
+ this.resolveDebug = resolveDebug;
88
+ const feedback = modelFeedback(result);
89
+ if (feedback !== undefined) {
90
+ this.feedback = new Container();
91
+ this.feedback.addChild(new Text(feedback, 0, 0));
92
+ }
93
+ }
94
+
95
+ render(width: number): string[] {
96
+ return !this.expanded && this.resolveDebug() && this.feedback
97
+ ? this.feedback.render(width)
98
+ : this.ordinary.render(width);
99
+ }
100
+
101
+ invalidate(): void {
102
+ this.ordinary.invalidate();
103
+ this.feedback?.invalidate();
104
+ }
105
+ }
106
+
43
107
  export function renderApplyPatchCall(
44
108
  args: ApplyPatchArgs,
45
109
  theme: Theme,
46
110
  context: ApplyPatchRenderContext,
47
111
  resolveBackground: CodexToolBackgroundResolver = () => DEFAULT_CONFIG.toolBackground,
112
+ resolveDebug: ApplyPatchDebugResolver = () => DEFAULT_CONFIG.applyPatchDebug,
48
113
  ): Component {
49
114
  const container = new Container();
50
- container.addChild(new Text(theme.fg("toolTitle", theme.bold("apply_patch")), 0, 0));
115
+ container.addChild(new ApplyPatchTitleComponent(theme, resolveDebug));
51
116
 
52
117
  if (context.isPartial) {
53
118
  const state = context.state;
@@ -96,23 +161,20 @@ export function renderApplyPatchResult(
96
161
  theme: Theme,
97
162
  context: ApplyPatchRenderContext,
98
163
  resolveBackground: CodexToolBackgroundResolver = () => DEFAULT_CONFIG.toolBackground,
164
+ resolveDebug: ApplyPatchDebugResolver = () => DEFAULT_CONFIG.applyPatchDebug,
99
165
  ): Component {
100
166
  if (options.isPartial) return new Container();
101
167
 
102
168
  const details = isApplyPatchDetails(result.details) ? result.details : undefined;
103
- const preview = isApplyPatchDetails(context.state.preview) ? context.state.preview : undefined;
104
- const renderDetails =
105
- details?.status === "failed" && preview
106
- ? {
107
- ...preview,
108
- status: "failed" as const,
109
- ...(details.error !== undefined ? { error: details.error } : {}),
110
- }
111
- : details;
112
-
113
- if (renderDetails) {
169
+
170
+ if (details) {
114
171
  return new CodexToolSurfaceComponent(
115
- new ApplyPatchDiffComponent(renderDetails, theme, context.cwd, context.expanded),
172
+ new ApplyPatchResultComponent(
173
+ new ApplyPatchDiffComponent(details, theme, context.cwd, context.expanded),
174
+ result,
175
+ context.expanded,
176
+ resolveDebug,
177
+ ),
116
178
  theme,
117
179
  {
118
180
  background: resolveBackground,
@@ -124,10 +186,14 @@ export function renderApplyPatchResult(
124
186
  }
125
187
  const text = context.isError ? theme.bold(theme.fg("error", "✘ Failed to apply patch")) : "";
126
188
  if (!text) return new Container();
127
- return new CodexToolSurfaceComponent(new Text(text, 0, 0), theme, {
128
- background: resolveBackground,
129
- status: "error",
130
- top: false,
131
- bottom: true,
132
- });
189
+ return new CodexToolSurfaceComponent(
190
+ new ApplyPatchResultComponent(new Text(text, 0, 0), result, context.expanded, resolveDebug),
191
+ theme,
192
+ {
193
+ background: resolveBackground,
194
+ status: "error",
195
+ top: false,
196
+ bottom: true,
197
+ },
198
+ );
133
199
  }
@@ -7,14 +7,30 @@ import {
7
7
  applyPatch,
8
8
  type ApplyPatchDetails,
9
9
  ApplyPatchExecutionError,
10
+ ApplyPatchInputError,
11
+ ApplyPatchVerificationError,
12
+ formatApplyPatchFailureSummary,
10
13
  formatApplyPatchModelOutput,
11
14
  formatApplyPatchSummary,
12
15
  } from "./apply-patch-engine.ts";
13
- import { renderApplyPatchCall, renderApplyPatchResult } from "./apply-patch-render.ts";
16
+ import {
17
+ type ApplyPatchDebugResolver,
18
+ renderApplyPatchCall,
19
+ renderApplyPatchResult,
20
+ } from "./apply-patch-render.ts";
14
21
 
15
22
  export {
16
23
  applyPatch,
17
24
  type ApplyPatchDetails,
25
+ type ApplyPatchExecutionFilesystem,
26
+ type ApplyPatchExecutionHooks,
27
+ type ApplyPatchFailureDetails,
28
+ type ApplyPatchInstructionDetails,
29
+ type ApplyPatchInstructionEffect,
30
+ type ApplyPatchInstructionReason,
31
+ type ApplyPatchInstructionReasonCode,
32
+ type ApplyPatchInstructionStatus,
33
+ type ApplyPatchFinalPathState,
18
34
  type AppliedPatchChange,
19
35
  ApplyPatchExecutionError,
20
36
  ApplyPatchInputError,
@@ -27,6 +43,7 @@ export {
27
43
  parsePatchDocument,
28
44
  previewPatch,
29
45
  } from "./apply-patch-engine.ts";
46
+ export type { FormatterMatchFailureDetails } from "./apply-patch-matcher.ts";
30
47
 
31
48
  export const APPLY_PATCH_TOOL_NAME = "apply_patch";
32
49
  export const APPLY_PATCH_INPUT_PROPERTY = "patch";
@@ -57,6 +74,7 @@ eof_line: "*** End of File" LF
57
74
  export default function registerApplyPatch(
58
75
  pi: ExtensionAPI,
59
76
  resolveToolBackground: CodexToolBackgroundResolver = () => DEFAULT_CONFIG.toolBackground,
77
+ resolveDebug: ApplyPatchDebugResolver = () => DEFAULT_CONFIG.applyPatchDebug,
60
78
  ): void {
61
79
  const failedDetails = new Map<string, ApplyPatchDetails>();
62
80
 
@@ -115,7 +133,7 @@ export default function registerApplyPatch(
115
133
  text: formatApplyPatchModelOutput(
116
134
  0,
117
135
  executionDurationMs(),
118
- formatApplyPatchSummary(details),
136
+ formatApplyPatchSummary(details, ctx.cwd),
119
137
  ),
120
138
  },
121
139
  ],
@@ -125,17 +143,35 @@ export default function registerApplyPatch(
125
143
  if (error instanceof ApplyPatchExecutionError) {
126
144
  failedDetails.set(toolCallId, error.details);
127
145
  throw new Error(
128
- formatApplyPatchModelOutput(1, executionDurationMs(), `${error.message}\n`),
146
+ formatApplyPatchModelOutput(
147
+ 1,
148
+ executionDurationMs(),
149
+ formatApplyPatchFailureSummary(error.details, ctx.cwd),
150
+ ),
129
151
  );
130
152
  }
153
+ if (error instanceof ApplyPatchVerificationError) {
154
+ failedDetails.set(toolCallId, error.details);
155
+ throw new Error(formatApplyPatchFailureSummary(error.details, ctx.cwd));
156
+ } else if (error instanceof ApplyPatchInputError && error.details) {
157
+ failedDetails.set(toolCallId, error.details);
158
+ throw new Error(formatApplyPatchFailureSummary(error.details, ctx.cwd));
159
+ }
131
160
  throw error;
132
161
  }
133
162
  },
134
163
  renderCall(args, theme, context) {
135
- return renderApplyPatchCall(args, theme, context, resolveToolBackground);
164
+ return renderApplyPatchCall(args, theme, context, resolveToolBackground, resolveDebug);
136
165
  },
137
166
  renderResult(result, options, theme, context) {
138
- return renderApplyPatchResult(result, options, theme, context, resolveToolBackground);
167
+ return renderApplyPatchResult(
168
+ result,
169
+ options,
170
+ theme,
171
+ context,
172
+ resolveToolBackground,
173
+ resolveDebug,
174
+ );
139
175
  },
140
176
  });
141
177
  }
@@ -9,6 +9,7 @@ import {
9
9
  clampThinkingLevel,
10
10
  createAssistantMessageEventStream,
11
11
  type AssistantMessage,
12
+ type AssistantMessageDiagnostic,
12
13
  type AssistantMessageEventStream,
13
14
  type Context,
14
15
  type Model,
@@ -28,6 +29,7 @@ import {
28
29
  providerHistory,
29
30
  searchCheckpoint,
30
31
  type CheckpointData,
32
+ type CompactionDecision,
31
33
  type GrammarToolInputProperties,
32
34
  } from "./compaction-checkpoint.ts";
33
35
  import {
@@ -126,6 +128,30 @@ type CodexTerminalState = {
126
128
  response?: JsonRecord;
127
129
  };
128
130
 
131
+ type CodexAttemptCapture = {
132
+ streamedItems: ResponsesItem[];
133
+ streamedToolCallKeys: Set<string>;
134
+ streamedCompletedToolCallKeys: Set<string>;
135
+ terminalItems?: ResponsesItem[];
136
+ };
137
+
138
+ type CodexToolCallAssessment = {
139
+ allComplete: boolean;
140
+ authoritativeCount: number;
141
+ hasToolCalls: boolean;
142
+ omittedStreamedCount: number;
143
+ terminalCount: number;
144
+ };
145
+
146
+ type CodexResponseDecision =
147
+ | "continue_no_tools"
148
+ | "preserve_terminal_error"
149
+ | "reject_terminal_stream_mismatch"
150
+ | "retry_original_input"
151
+ | "return_length_incomplete_call"
152
+ | "return_terminal"
153
+ | "return_tool_use";
154
+
129
155
  export type CodexResponseRetryPolicy = {
130
156
  maxRetries: number;
131
157
  baseDelayMs: number;
@@ -187,16 +213,143 @@ function nativeOverrideRequired(
187
213
  );
188
214
  }
189
215
 
216
+ function isToolCallItem(item: ResponsesItem): boolean {
217
+ return item.type === "function_call" || item.type === "custom_tool_call";
218
+ }
219
+
220
+ function toolCallKey(item: ResponsesItem): string {
221
+ const callId = typeof item["call_id"] === "string" ? item["call_id"] : undefined;
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";
253
+ }
254
+
255
+ function assessAttemptToolCalls(
256
+ items: readonly ResponsesItem[],
257
+ capture: CodexAttemptCapture,
258
+ terminalType: CodexTerminalState["type"],
259
+ ): CodexToolCallAssessment {
260
+ const calls = items.filter(isToolCallItem);
261
+ const authoritativeKeys = new Set(calls.map(toolCallKey));
262
+ const omittedStreamedCount = [...capture.streamedToolCallKeys].filter(
263
+ (key) => !authoritativeKeys.has(key),
264
+ ).length;
265
+ const hasToolCalls = calls.length > 0 || capture.streamedToolCallKeys.size > 0;
266
+ return {
267
+ hasToolCalls,
268
+ allComplete:
269
+ hasToolCalls &&
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,
276
+ };
277
+ }
278
+
279
+ function outputItemTypeCounts(items: readonly ResponsesItem[]): Record<string, number> {
280
+ const counts: Record<string, number> = {};
281
+ for (const item of items) {
282
+ const type = item.type ?? "message";
283
+ counts[type] = (counts[type] ?? 0) + 1;
284
+ }
285
+ return counts;
286
+ }
287
+
288
+ function responseDecisionDiagnostic(options: {
289
+ attempt: number;
290
+ attemptItems: readonly ResponsesItem[];
291
+ capture: CodexAttemptCapture;
292
+ decision: CodexResponseDecision;
293
+ incompleteReason?: string;
294
+ terminalState: CodexTerminalState;
295
+ toolCalls: CodexToolCallAssessment;
296
+ }): AssistantMessageDiagnostic | undefined {
297
+ const endTurn =
298
+ typeof options.terminalState.response?.["end_turn"] === "boolean"
299
+ ? options.terminalState.response["end_turn"]
300
+ : undefined;
301
+ const nontrivial =
302
+ options.attempt > 1 ||
303
+ options.terminalState.type !== "response.completed" ||
304
+ endTurn === false ||
305
+ options.toolCalls.hasToolCalls ||
306
+ options.toolCalls.omittedStreamedCount > 0;
307
+ if (!nontrivial) return undefined;
308
+
309
+ return {
310
+ type: "codex_response_decision",
311
+ timestamp: Date.now(),
312
+ details: {
313
+ attempt: options.attempt,
314
+ terminalType: options.terminalState.type ?? "missing",
315
+ ...(options.incompleteReason ? { incompleteReason: options.incompleteReason } : {}),
316
+ ...(endTurn === undefined ? {} : { endTurn }),
317
+ itemSource: options.capture.terminalItems ? "terminal" : "stream-fallback",
318
+ outputItemTypes: outputItemTypeCounts(options.attemptItems),
319
+ streamedCallsStarted: options.capture.streamedToolCallKeys.size,
320
+ streamedCallsCompleted: options.capture.streamedCompletedToolCallKeys.size,
321
+ terminalCalls: options.toolCalls.terminalCount,
322
+ authoritativeCalls: options.toolCalls.authoritativeCount,
323
+ terminalOmittedStreamedCalls: options.toolCalls.omittedStreamedCount,
324
+ allCallsComplete: options.toolCalls.allComplete,
325
+ decision: options.decision,
326
+ },
327
+ };
328
+ }
329
+
190
330
  function captureRawEvents(
191
331
  events: AsyncIterable<JsonRecord>,
192
- items: ResponsesItem[],
332
+ capture: CodexAttemptCapture,
193
333
  terminalState?: CodexTerminalState,
194
334
  ): AsyncIterable<JsonRecord> {
195
335
  return {
196
336
  async *[Symbol.asyncIterator]() {
197
337
  for await (const event of events) {
338
+ if (
339
+ event.type === "response.output_item.added" &&
340
+ isResponsesItem(event.item) &&
341
+ isToolCallItem(event.item)
342
+ ) {
343
+ capture.streamedToolCallKeys.add(toolCallKey(event.item));
344
+ }
198
345
  if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
199
- items.push(structuredClone(event.item));
346
+ const item = structuredClone(event.item);
347
+ capture.streamedItems.push(item);
348
+ if (isToolCallItem(item)) {
349
+ const key = toolCallKey(item);
350
+ capture.streamedToolCallKeys.add(key);
351
+ capture.streamedCompletedToolCallKeys.add(key);
352
+ }
200
353
  }
201
354
  if (
202
355
  (event.type === "response.completed" ||
@@ -205,10 +358,9 @@ function captureRawEvents(
205
358
  isObject(event.response) &&
206
359
  Array.isArray(event.response["output"])
207
360
  ) {
208
- const terminalItems = event.response["output"].filter(isResponsesItem);
209
- if (terminalItems.length > 0) {
210
- items.splice(0, items.length, ...terminalItems.map((item) => structuredClone(item)));
211
- }
361
+ capture.terminalItems = event.response["output"]
362
+ .filter(isResponsesItem)
363
+ .map((item) => structuredClone(item));
212
364
  }
213
365
  if (
214
366
  terminalState &&
@@ -808,6 +960,7 @@ export class CodexProviderRuntime {
808
960
  grammarToolInputProperties: GrammarToolInputProperties;
809
961
  priority: boolean;
810
962
  compactionMetadata: CodexCompactionMetadata;
963
+ compactionDecision: CompactionDecision;
811
964
  agentTurn?: ActiveAgentTurn;
812
965
  responsesLiteEnabled?: boolean;
813
966
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
@@ -881,6 +1034,7 @@ export class CodexProviderRuntime {
881
1034
  options.history,
882
1035
  compacted.item,
883
1036
  options.postCompactionTail,
1037
+ options.compactionDecision,
884
1038
  ),
885
1039
  ...(compacted.usage ? { usage: compacted.usage } : {}),
886
1040
  };
@@ -953,6 +1107,7 @@ export class CodexProviderRuntime {
953
1107
  grammarToolInputProperties,
954
1108
  priority: scope.config.fastMode,
955
1109
  compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
1110
+ compactionDecision: { reason: "provider-boundary", willRetry: true },
956
1111
  agentTurn,
957
1112
  responsesLiteEnabled,
958
1113
  });
@@ -1103,12 +1258,17 @@ export class CodexProviderRuntime {
1103
1258
  let responseRetries = 0;
1104
1259
  while (true) {
1105
1260
  responseRequests += 1;
1106
- const attemptItems: ResponsesItem[] = [];
1261
+ const attemptCapture: CodexAttemptCapture = {
1262
+ streamedItems: [],
1263
+ streamedToolCallKeys: new Set(),
1264
+ streamedCompletedToolCallKeys: new Set(),
1265
+ };
1107
1266
  const terminalState: CodexTerminalState = {};
1108
1267
  const attemptState: CodexStreamAttemptState = {
1109
1268
  startedContentIndexes: new Set(),
1110
1269
  completedContentIndexes: new Set(),
1111
1270
  };
1271
+ const contentLengthBeforeAttempt = output.content.length;
1112
1272
  const usageBeforeAttempt = structuredClone(output.usage);
1113
1273
  output.usage = emptyUsage();
1114
1274
  try {
@@ -1116,7 +1276,7 @@ export class CodexProviderRuntime {
1116
1276
  startOnFirstEvent(
1117
1277
  captureRawEvents(
1118
1278
  this.transport.request(model, body, transportRequestOptions),
1119
- attemptItems,
1279
+ attemptCapture,
1120
1280
  terminalState,
1121
1281
  ),
1122
1282
  emitStart,
@@ -1140,12 +1300,87 @@ export class CodexProviderRuntime {
1140
1300
  throw error;
1141
1301
  }
1142
1302
  output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
1143
- rawItems.push(...attemptItems.map((item) => structuredClone(item)));
1303
+ // A terminal response.output is the complete provider snapshot when present.
1304
+ // Stream-completed items are the fallback for transports that omit that field.
1305
+ const attemptItems = attemptCapture.terminalItems ?? attemptCapture.streamedItems;
1306
+ const toolCalls = assessAttemptToolCalls(
1307
+ attemptItems,
1308
+ attemptCapture,
1309
+ terminalState.type,
1310
+ );
1311
+ const incompleteDetails = isObject(terminalState.response?.["incomplete_details"])
1312
+ ? terminalState.response["incomplete_details"]
1313
+ : undefined;
1314
+ const incompleteReason =
1315
+ typeof incompleteDetails?.["reason"] === "string"
1316
+ ? incompleteDetails["reason"]
1317
+ : undefined;
1318
+ const maxOutputIncomplete =
1319
+ terminalState.type === "response.incomplete" &&
1320
+ incompleteReason === "max_output_tokens";
1321
+ const recordDecision = (decision: CodexResponseDecision): void => {
1322
+ const diagnostic = responseDecisionDiagnostic({
1323
+ attempt: responseRequests,
1324
+ attemptItems,
1325
+ capture: attemptCapture,
1326
+ decision,
1327
+ ...(incompleteReason ? { incompleteReason } : {}),
1328
+ terminalState,
1329
+ toolCalls,
1330
+ });
1331
+ if (diagnostic) output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
1332
+ };
1333
+
1334
+ // Tool calls create a client-execution boundary. Never append them to an
1335
+ // internal provider continuation before Pi can return the linked outputs.
1336
+ if (toolCalls.hasToolCalls) {
1337
+ if (
1338
+ toolCalls.allComplete &&
1339
+ ((terminalState.type === "response.completed" && output.stopReason === "toolUse") ||
1340
+ maxOutputIncomplete)
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;
1348
+ }
1349
+
1350
+ output.content.splice(contentLengthBeforeAttempt);
1351
+ if (
1352
+ terminalState.type === "response.failed" &&
1353
+ retryableResponseFailure(terminalState.response) &&
1354
+ responseRetries < this.responseRetryPolicy.maxRetries
1355
+ ) {
1356
+ responseRetries += 1;
1357
+ output.stopReason = "pending";
1358
+ delete output.errorMessage;
1359
+ delete output.rawStopReason;
1360
+ recordDecision("retry_original_input");
1361
+ await waitForResponseRetry(
1362
+ responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
1363
+ requestOptions.signal,
1364
+ );
1365
+ continue;
1366
+ }
1367
+ if (terminalState.type === "response.completed" && output.stopReason !== "error") {
1368
+ output.stopReason = "length";
1369
+ output.rawStopReason = "incomplete.tool_call";
1370
+ delete output.errorMessage;
1371
+ }
1372
+ recordDecision(
1373
+ toolCalls.omittedStreamedCount > 0
1374
+ ? "reject_terminal_stream_mismatch"
1375
+ : output.stopReason === "length"
1376
+ ? "return_length_incomplete_call"
1377
+ : "preserve_terminal_error",
1378
+ );
1379
+ break;
1380
+ }
1144
1381
 
1145
1382
  const nextBody = continueResponseBody(body, attemptItems);
1146
- const attemptHasToolCall = [...attemptState.completedContentIndexes].some(
1147
- (index) => output.content[index]?.type === "toolCall",
1148
- );
1383
+ rawItems.push(...attemptItems.map((item) => structuredClone(item)));
1149
1384
  discardIncompleteAttemptContent(output, attemptState);
1150
1385
  const retryableTerminal =
1151
1386
  terminalState.type === "response.incomplete" ||
@@ -1161,6 +1396,7 @@ export class CodexProviderRuntime {
1161
1396
  output.stopReason = "pending";
1162
1397
  delete output.errorMessage;
1163
1398
  delete output.rawStopReason;
1399
+ recordDecision("continue_no_tools");
1164
1400
  await waitForResponseRetry(
1165
1401
  responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
1166
1402
  requestOptions.signal,
@@ -1170,8 +1406,7 @@ export class CodexProviderRuntime {
1170
1406
 
1171
1407
  if (
1172
1408
  terminalState.type === "response.completed" &&
1173
- terminalState.response?.["end_turn"] === false &&
1174
- !attemptHasToolCall
1409
+ terminalState.response?.["end_turn"] === false
1175
1410
  ) {
1176
1411
  if (!nextBody) {
1177
1412
  throw new Error(
@@ -1183,8 +1418,10 @@ export class CodexProviderRuntime {
1183
1418
  output.stopReason = "pending";
1184
1419
  delete output.errorMessage;
1185
1420
  delete output.rawStopReason;
1421
+ recordDecision("continue_no_tools");
1186
1422
  continue;
1187
1423
  }
1424
+ recordDecision("return_terminal");
1188
1425
  break;
1189
1426
  }
1190
1427
  if (requestOptions.signal?.aborted) throw new Error("Request was aborted");
@@ -1285,6 +1522,7 @@ export class CodexProviderRuntime {
1285
1522
  template: JsonRecord;
1286
1523
  priority: boolean;
1287
1524
  compactionMetadata: CodexCompactionMetadata;
1525
+ compactionDecision: CompactionDecision;
1288
1526
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
1289
1527
  validateCodexAuthentication(options.model, options.requestOptions.apiKey);
1290
1528
  const release = await this.acquireRequest(