pi-openai-codex-compat 0.0.7-alpha.0 → 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.
@@ -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 {
@@ -113,6 +115,7 @@ type ActiveAgentTurn = {
113
115
  turnId: string;
114
116
  startedAtUnixMs: number;
115
117
  turnState: CodexTurnState;
118
+ pendingPostToolDisposition?: CodexPostToolDisposition;
116
119
  };
117
120
 
118
121
  type CodexCompat = {
@@ -126,6 +129,32 @@ type CodexTerminalState = {
126
129
  response?: JsonRecord;
127
130
  };
128
131
 
132
+ type CodexAttemptCapture = {
133
+ streamedItems: ResponsesItem[];
134
+ streamedToolCallIndexes: Set<number>;
135
+ streamedCompletedToolCallIndexes: Set<number>;
136
+ };
137
+
138
+ type CodexToolCallAssessment = {
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";
150
+ };
151
+
152
+ type CodexResponseDecision =
153
+ | "continue_no_tools"
154
+ | "retry_original_input"
155
+ | "return_terminal"
156
+ | "return_tool_use";
157
+
129
158
  export type CodexResponseRetryPolicy = {
130
159
  maxRetries: number;
131
160
  baseDelayMs: number;
@@ -187,27 +216,100 @@ function nativeOverrideRequired(
187
216
  );
188
217
  }
189
218
 
219
+ function isToolCallItem(item: ResponsesItem): boolean {
220
+ return item.type === "function_call" || item.type === "custom_tool_call";
221
+ }
222
+
223
+ function eventOutputIndex(event: JsonRecord): number {
224
+ return typeof event["output_index"] === "number" ? event["output_index"] : 0;
225
+ }
226
+
227
+ function assessAttemptToolCalls(
228
+ items: readonly ResponsesItem[],
229
+ capture: CodexAttemptCapture,
230
+ ): CodexToolCallAssessment {
231
+ const completedCount = items.filter(isToolCallItem).length;
232
+ const discardedPartialCount = [...capture.streamedToolCallIndexes].filter(
233
+ (index) => !capture.streamedCompletedToolCallIndexes.has(index),
234
+ ).length;
235
+ return {
236
+ completedCount,
237
+ discardedPartialCount,
238
+ hasCompletedCalls: completedCount > 0,
239
+ };
240
+ }
241
+
242
+ function outputItemTypeCounts(items: readonly ResponsesItem[]): Record<string, number> {
243
+ const counts: Record<string, number> = {};
244
+ for (const item of items) {
245
+ const type = item.type ?? "message";
246
+ counts[type] = (counts[type] ?? 0) + 1;
247
+ }
248
+ return counts;
249
+ }
250
+
251
+ function responseDecisionDiagnostic(options: {
252
+ attempt: number;
253
+ attemptItems: readonly ResponsesItem[];
254
+ capture: CodexAttemptCapture;
255
+ decision: CodexResponseDecision;
256
+ incompleteReason?: string;
257
+ postToolDisposition?: "continue" | "error" | "retry";
258
+ terminalState: CodexTerminalState;
259
+ toolCalls: CodexToolCallAssessment;
260
+ }): AssistantMessageDiagnostic | undefined {
261
+ const endTurn =
262
+ typeof options.terminalState.response?.["end_turn"] === "boolean"
263
+ ? options.terminalState.response["end_turn"]
264
+ : undefined;
265
+ const nontrivial =
266
+ options.attempt > 1 ||
267
+ options.terminalState.type !== "response.completed" ||
268
+ endTurn === false ||
269
+ options.capture.streamedToolCallIndexes.size > 0;
270
+ if (!nontrivial) return undefined;
271
+
272
+ return {
273
+ type: "codex_response_decision",
274
+ timestamp: Date.now(),
275
+ details: {
276
+ attempt: options.attempt,
277
+ terminalType: options.terminalState.type ?? "missing",
278
+ ...(options.incompleteReason ? { incompleteReason: options.incompleteReason } : {}),
279
+ ...(endTurn === undefined ? {} : { endTurn }),
280
+ outputItemTypes: outputItemTypeCounts(options.attemptItems),
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 } : {}),
286
+ decision: options.decision,
287
+ },
288
+ };
289
+ }
290
+
190
291
  function captureRawEvents(
191
292
  events: AsyncIterable<JsonRecord>,
192
- items: ResponsesItem[],
293
+ capture: CodexAttemptCapture,
193
294
  terminalState?: CodexTerminalState,
194
295
  ): AsyncIterable<JsonRecord> {
195
296
  return {
196
297
  async *[Symbol.asyncIterator]() {
197
298
  for await (const event of events) {
198
- if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
199
- items.push(structuredClone(event.item));
200
- }
201
299
  if (
202
- (event.type === "response.completed" ||
203
- event.type === "response.incomplete" ||
204
- event.type === "response.failed") &&
205
- isObject(event.response) &&
206
- Array.isArray(event.response["output"])
300
+ event.type === "response.output_item.added" &&
301
+ isResponsesItem(event.item) &&
302
+ isToolCallItem(event.item)
207
303
  ) {
208
- const terminalItems = event.response["output"].filter(isResponsesItem);
209
- if (terminalItems.length > 0) {
210
- items.splice(0, items.length, ...terminalItems.map((item) => structuredClone(item)));
304
+ capture.streamedToolCallIndexes.add(eventOutputIndex(event));
305
+ }
306
+ if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
307
+ const item = structuredClone(event.item);
308
+ capture.streamedItems.push(item);
309
+ if (isToolCallItem(item)) {
310
+ const index = eventOutputIndex(event);
311
+ capture.streamedToolCallIndexes.add(index);
312
+ capture.streamedCompletedToolCallIndexes.add(index);
211
313
  }
212
314
  }
213
315
  if (
@@ -310,6 +412,48 @@ function retryableResponseFailure(response: JsonRecord | undefined): boolean {
310
412
  );
311
413
  }
312
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
+
313
457
  function responseRetryDelayMs(baseDelayMs: number, attempt: number): number {
314
458
  if (baseDelayMs <= 0) return 0;
315
459
  const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
@@ -808,6 +952,7 @@ export class CodexProviderRuntime {
808
952
  grammarToolInputProperties: GrammarToolInputProperties;
809
953
  priority: boolean;
810
954
  compactionMetadata: CodexCompactionMetadata;
955
+ compactionDecision: CompactionDecision;
811
956
  agentTurn?: ActiveAgentTurn;
812
957
  responsesLiteEnabled?: boolean;
813
958
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
@@ -881,6 +1026,7 @@ export class CodexProviderRuntime {
881
1026
  options.history,
882
1027
  compacted.item,
883
1028
  options.postCompactionTail,
1029
+ options.compactionDecision,
884
1030
  ),
885
1031
  ...(compacted.usage ? { usage: compacted.usage } : {}),
886
1032
  };
@@ -953,6 +1099,7 @@ export class CodexProviderRuntime {
953
1099
  grammarToolInputProperties,
954
1100
  priority: scope.config.fastMode,
955
1101
  compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
1102
+ compactionDecision: { reason: "provider-boundary", willRetry: true },
956
1103
  agentTurn,
957
1104
  responsesLiteEnabled,
958
1105
  });
@@ -1014,6 +1161,26 @@ export class CodexProviderRuntime {
1014
1161
  requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
1015
1162
  const agentTurn = this.agentTurn(runtimeSessionId);
1016
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
+ }
1017
1184
  const grammarToolInputProperties = createGrammarToolInputProperties(
1018
1185
  context.tools,
1019
1186
  (model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
@@ -1100,10 +1267,14 @@ export class CodexProviderRuntime {
1100
1267
  },
1101
1268
  };
1102
1269
  let responseRequests = 0;
1103
- let responseRetries = 0;
1270
+ let responseRetries = carriedResponseRetries;
1104
1271
  while (true) {
1105
1272
  responseRequests += 1;
1106
- const attemptItems: ResponsesItem[] = [];
1273
+ const attemptCapture: CodexAttemptCapture = {
1274
+ streamedItems: [],
1275
+ streamedToolCallIndexes: new Set(),
1276
+ streamedCompletedToolCallIndexes: new Set(),
1277
+ };
1107
1278
  const terminalState: CodexTerminalState = {};
1108
1279
  const attemptState: CodexStreamAttemptState = {
1109
1280
  startedContentIndexes: new Set(),
@@ -1116,7 +1287,7 @@ export class CodexProviderRuntime {
1116
1287
  startOnFirstEvent(
1117
1288
  captureRawEvents(
1118
1289
  this.transport.request(model, body, transportRequestOptions),
1119
- attemptItems,
1290
+ attemptCapture,
1120
1291
  terminalState,
1121
1292
  ),
1122
1293
  emitStart,
@@ -1140,12 +1311,71 @@ export class CodexProviderRuntime {
1140
1311
  throw error;
1141
1312
  }
1142
1313
  output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
1143
- rawItems.push(...attemptItems.map((item) => structuredClone(item)));
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);
1318
+ const incompleteDetails = isObject(terminalState.response?.["incomplete_details"])
1319
+ ? terminalState.response["incomplete_details"]
1320
+ : undefined;
1321
+ const incompleteReason =
1322
+ typeof incompleteDetails?.["reason"] === "string"
1323
+ ? incompleteDetails["reason"]
1324
+ : undefined;
1325
+ const recordDecision = (
1326
+ decision: CodexResponseDecision,
1327
+ postToolDisposition?: "continue" | "error" | "retry",
1328
+ ): void => {
1329
+ const diagnostic = responseDecisionDiagnostic({
1330
+ attempt: responseRequests,
1331
+ attemptItems,
1332
+ capture: attemptCapture,
1333
+ decision,
1334
+ ...(incompleteReason ? { incompleteReason } : {}),
1335
+ ...(postToolDisposition ? { postToolDisposition } : {}),
1336
+ terminalState,
1337
+ toolCalls,
1338
+ });
1339
+ if (diagnostic) output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
1340
+ };
1341
+
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.");
1349
+ }
1350
+ let postToolDisposition: "continue" | "error" | "retry" = "continue";
1351
+ if (
1352
+ terminalState.type === "response.incomplete" ||
1353
+ terminalState.type === "response.failed"
1354
+ ) {
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
+ };
1368
+ }
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);
1374
+ break;
1375
+ }
1144
1376
 
1145
1377
  const nextBody = continueResponseBody(body, attemptItems);
1146
- const attemptHasToolCall = [...attemptState.completedContentIndexes].some(
1147
- (index) => output.content[index]?.type === "toolCall",
1148
- );
1378
+ rawItems.push(...attemptItems.map((item) => structuredClone(item)));
1149
1379
  discardIncompleteAttemptContent(output, attemptState);
1150
1380
  const retryableTerminal =
1151
1381
  terminalState.type === "response.incomplete" ||
@@ -1161,6 +1391,9 @@ export class CodexProviderRuntime {
1161
1391
  output.stopReason = "pending";
1162
1392
  delete output.errorMessage;
1163
1393
  delete output.rawStopReason;
1394
+ recordDecision(
1395
+ attemptItems.length === 0 ? "retry_original_input" : "continue_no_tools",
1396
+ );
1164
1397
  await waitForResponseRetry(
1165
1398
  responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
1166
1399
  requestOptions.signal,
@@ -1170,8 +1403,7 @@ export class CodexProviderRuntime {
1170
1403
 
1171
1404
  if (
1172
1405
  terminalState.type === "response.completed" &&
1173
- terminalState.response?.["end_turn"] === false &&
1174
- !attemptHasToolCall
1406
+ terminalState.response?.["end_turn"] === false
1175
1407
  ) {
1176
1408
  if (!nextBody) {
1177
1409
  throw new Error(
@@ -1183,8 +1415,10 @@ export class CodexProviderRuntime {
1183
1415
  output.stopReason = "pending";
1184
1416
  delete output.errorMessage;
1185
1417
  delete output.rawStopReason;
1418
+ recordDecision("continue_no_tools");
1186
1419
  continue;
1187
1420
  }
1421
+ recordDecision("return_terminal");
1188
1422
  break;
1189
1423
  }
1190
1424
  if (requestOptions.signal?.aborted) throw new Error("Request was aborted");
@@ -1285,6 +1519,7 @@ export class CodexProviderRuntime {
1285
1519
  template: JsonRecord;
1286
1520
  priority: boolean;
1287
1521
  compactionMetadata: CodexCompactionMetadata;
1522
+ compactionDecision: CompactionDecision;
1288
1523
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
1289
1524
  validateCodexAuthentication(options.model, options.requestOptions.apiKey);
1290
1525
  const release = await this.acquireRequest(