pi-openai-codex-compat 0.0.7 → 0.0.9
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 +14 -0
- package/README.md +16 -16
- package/extensions/openai-codex-compat/codex-provider.ts +258 -145
- package/extensions/openai-codex-compat/codex-stream.ts +9 -22
- package/extensions/openai-codex-compat/codex-transport.ts +0 -10
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +24 -11
- package/extensions/openai-codex-compat/config.ts +3 -3
- package/extensions/openai-codex-compat/native-history.ts +114 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.9 - 2026-08-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserve deferred incomplete and failed Codex response handling across linked tool execution without requiring session affinity or Pi agent-turn hooks.
|
|
10
|
+
- Preserve provider items committed before a later context-overflow subrequest, compact the validated prefix, and retry automatically from the native checkpoint.
|
|
11
|
+
- Split successful Codex follow-up sampling at percentage-compaction boundaries so Pi records the committed prefix, native checkpoint, and continued response in chronological order without synthetic model input.
|
|
12
|
+
|
|
13
|
+
## 0.0.8 - 2026-08-16
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
5
19
|
## 0.0.7 - 2026-08-16
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -35,19 +35,19 @@ The compatibility baseline is official Codex CLI `0.146.0`, released July 29, 20
|
|
|
35
35
|
|
|
36
36
|
### Configurable defaults that differ from Codex
|
|
37
37
|
|
|
38
|
-
| Area | This package by default | Official Codex | Configuration
|
|
39
|
-
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
-
| Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting. | Uses `high`. | `imageDetail`: `auto`, `low`, `high`, or `original`.
|
|
41
|
-
| Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes. | Stable and enabled by default, but additionally gated by plan, model, provider, authentication, image-generation, and namespace capabilities. | `imageGeneration`: boolean.
|
|
42
|
-
| Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description. | Enabled by default for `gpt-5.6-sol` through Responses Lite; otherwise subject to standalone-search feature and runtime gates. | `webRun`: boolean.
|
|
43
|
-
| Hosted web search | Disabled by default; when enabled, injected only for ordinary Responses while `web.run` is inactive. Responses Lite omits hosted tools. | Omitted for `gpt-5.6-sol` while standalone `web.run` is available; otherwise defaults to cached mode when hosted search is supported. | `webRun` and `webSearch`: `disabled`, `cached`, `indexed`, or `live`.
|
|
44
|
-
| Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools. | Chooses its tool surface from model metadata and runtime capabilities; there are no Pi `edit` or `write` tools to suppress. | `applyPatch`: boolean.
|
|
45
|
-
| `apply_patch` debug output | Disabled; collapsed results show the normal visual summary and instruction rows. | Not applicable to Pi's tool-result renderer. | `applyPatchDebug`: boolean.
|
|
46
|
-
| Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools. | Uses Codex's own TUI activity cells rather than Pi tool rows. | `toolBackground`: `subtle`, `status`, or `none`.
|
|
47
|
-
| Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured. | Tracks Codex's model/token-budget state before and between sampling steps. | `autoCompactAtPercent`: percentage or unset. Pi's
|
|
48
|
-
| Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier.
|
|
49
|
-
| Responses Lite | Disabled; supported GPT-5.6 models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite.
|
|
50
|
-
| Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries; omits the default GPT-5.6 standard mode and sends `reasoning.mode` only for pro mode. | Resolves these controls through Codex configuration, model metadata, and turn state. | `textVerbosity`, `reasoningSummary`, and `reasoningMode`.
|
|
38
|
+
| Area | This package by default | Official Codex | Configuration |
|
|
39
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting. | Uses `high`. | `imageDetail`: `auto`, `low`, `high`, or `original`. |
|
|
41
|
+
| Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes. | Stable and enabled by default, but additionally gated by plan, model, provider, authentication, image-generation, and namespace capabilities. | `imageGeneration`: boolean. |
|
|
42
|
+
| Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description. | Enabled by default for `gpt-5.6-sol` through Responses Lite; otherwise subject to standalone-search feature and runtime gates. | `webRun`: boolean. |
|
|
43
|
+
| Hosted web search | Disabled by default; when enabled, injected only for ordinary Responses while `web.run` is inactive. Responses Lite omits hosted tools. | Omitted for `gpt-5.6-sol` while standalone `web.run` is available; otherwise defaults to cached mode when hosted search is supported. | `webRun` and `webSearch`: `disabled`, `cached`, `indexed`, or `live`. |
|
|
44
|
+
| Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools. | Chooses its tool surface from model metadata and runtime capabilities; there are no Pi `edit` or `write` tools to suppress. | `applyPatch`: boolean. |
|
|
45
|
+
| `apply_patch` debug output | Disabled; collapsed results show the normal visual summary and instruction rows. | Not applicable to Pi's tool-result renderer. | `applyPatchDebug`: boolean. |
|
|
46
|
+
| Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools. | Uses Codex's own TUI activity cells rather than Pi tool rows. | `toolBackground`: `subtle`, `status`, or `none`. |
|
|
47
|
+
| Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured. | Tracks Codex's model/token-budget state before and between sampling steps. | `autoCompactAtPercent`: percentage or unset. Mid-response percentage boundaries use Pi's bounded compact-and-continue lifecycle, so Pi auto-compaction must remain enabled. |
|
|
48
|
+
| Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
|
|
49
|
+
| Responses Lite | Disabled; supported GPT-5.6 models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
|
|
50
|
+
| Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries; omits the default GPT-5.6 standard mode and sends `reasoning.mode` only for pro mode. | Resolves these controls through Codex configuration, model metadata, and turn state. | `textVerbosity`, `reasoningSummary`, and `reasoningMode`. |
|
|
51
51
|
|
|
52
52
|
`web.run` is a reserved GPT-5.6 tool name. Its declaration therefore reproduces the complete current Codex post-normalization `SearchCommands` schema and official tool description instead of using Pi's normal compact tool schema. This intentionally omits generated annotations such as `format` and `minimum` that Codex removes before sending the declaration to Responses.
|
|
53
53
|
|
|
@@ -61,9 +61,9 @@ The compatibility baseline is official Codex CLI `0.146.0`, released July 29, 20
|
|
|
61
61
|
| System instructions | Pi rebuilds the current system prompt. Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Normal Pi history does not store it as replayed system/developer input. `/reload` updates the next request without rewriting old checkpoints. |
|
|
62
62
|
| Turn metadata | Requests send a persisted installation id plus Pi-derived session, thread, context-window, turn, source, sandbox, request-kind, and nested compaction-operation metadata in `client_metadata` and compatible headers. The in-memory context-window number advances after successful compaction. One turn id is reused throughout a Pi agent run, while prewarm has its own id. First-party requests also carry Codex's model-and-tier routing hint. The provider captures the server-issued `x-codex-turn-state` once per agent run, replays it on WebSocket retries, SSE requests, and WebSocket-to-SSE fallback, and records all identity values in transport diagnostics. Pi does not reconstruct prior window number after extension reload/session resume or reproduce workspace Git/parent/subagent/Code Mode metadata. Each marked Pi tree branch receives its own persisted thread UUID. |
|
|
63
63
|
| Cache preparation | Before the first cache-enabled WebSocket turn, the package prewarms only the stable instruction/tool prefix: ordinary Responses uses empty `input`, while Responses Lite uses `additional_tools` plus the developer instructions. The first generated request then contributes only dynamic conversation input to the continuation. No explicit prompt-cache breakpoints are added. |
|
|
64
|
-
| Mid-turn compaction | Provider-boundary percentage compaction installs a checkpoint and continues
|
|
64
|
+
| Mid-turn compaction | Provider-boundary percentage compaction preserves a successful `end_turn:false` prefix as its own Pi assistant message, installs a checkpoint, and continues without synthetic model input. Pi threshold compaction normally runs after the agent response; after Codex output-token truncation, the extension queues a hidden continuation so threshold compaction completes before sampling resumes. Official Codex owns this sampling and compaction loop directly. |
|
|
65
65
|
| Provider-owned follow-up | Completed responses with `end_turn: false` continue immediately from completed native output without synthetic user input. Retryable `response.failed` and all `response.incomplete` events are resampled with the official five-retry stream budget, preserving completed output and cumulative usage while excluding unfinished attempt content. A `max_output_tokens` response that exhausts this budget still becomes Pi `stopReason: "length"` and uses the extension's unbounded host-level continuation recovery. |
|
|
66
|
-
| Compaction lifecycle events |
|
|
66
|
+
| Compaction lifecycle events | Pre-turn percentage compaction writes through Pi's mutable session manager and cannot emit Pi's internal `session_compact` event through the public extension API. Mid-response percentage boundaries and manual, threshold, or overflow compactions initiated by Pi emit the normal lifecycle. |
|
|
67
67
|
| Header hooks | An internal percentage-compaction request reuses the already transformed provider headers. It cannot independently rerun Pi's `before_provider_headers` hook. |
|
|
68
68
|
| Native retained context | Deliberately differs from current Codex. The package retains recent user/developer/system messages under the 64k budget before the opaque compaction item. Current Codex applies a second installed-history filter that drops developer/system wrappers and non-real-user messages, can retain eligible structured agent commentary, and trims oversized function outputs before compaction. Pi keeps its existing checkpoint shape by design. |
|
|
69
69
|
| Tool namespaces | Responses Lite groups Pi's ordinary function/custom declarations into upstream's canonical `functions` namespace and maps that default namespace back to bare Pi names. Pi registers dotted names such as `web.run` as exact flat identifiers, so the provider converts only the fixed extension-owned allowlist into non-default Responses namespace/member identities and rejects unknown or ambiguously flat namespaced calls. |
|
|
@@ -198,7 +198,7 @@ Defaults:
|
|
|
198
198
|
| `imageGeneration` | boolean | `true` | Enables the extension-owned `image_gen.imagegen` tool on selected `openai-codex` models. |
|
|
199
199
|
| `imageDetail` | `auto`, `low`, `high`, `original` | `auto` | Sets `input_image.detail` when an image tool result is sent back to the model. It does not change `gpt-image-2` generation quality. |
|
|
200
200
|
| `webRun` | boolean | `false` | Enables the extension-owned `web.run` tool on selected `openai-codex` models. When active, it replaces hosted `web_search` in the Responses tool list. |
|
|
201
|
-
| `autoCompactAtPercent` | number greater than `0` and at most `100`, or `null` | unset | Adds provider-boundary compaction independently of Pi's normal reserve-token threshold. A project value of `null` disables a global percentage threshold.
|
|
201
|
+
| `autoCompactAtPercent` | number greater than `0` and at most `100`, or `null` | unset | Adds provider-boundary compaction independently of Pi's normal reserve-token threshold. Mid-response boundaries require Pi auto-compaction. A project value of `null` disables a global percentage threshold. |
|
|
202
202
|
| `webSearch` | `disabled`, `cached`, `indexed`, `live` | `disabled` | Controls hosted search and standalone-search external access. `disabled` removes hosted search but leaves an independently enabled `web.run` in cached-only mode; `indexed` prefers indexed content; `live` permits live external access. |
|
|
203
203
|
| `textVerbosity` | `low`, `medium`, `high` | `low` | Sets Responses API `text.verbosity`. |
|
|
204
204
|
| `reasoningSummary` | `auto`, `concise`, `detailed`, `off` | `auto` | Sets `reasoning.summary` when reasoning is enabled; `off` omits the summary parameter. |
|
|
@@ -61,7 +61,11 @@ import {
|
|
|
61
61
|
type CodexWebSocketResponseHandle,
|
|
62
62
|
} from "./codex-transport.ts";
|
|
63
63
|
import { DEFAULT_CONFIG, type CodexCompatConfig, type ImageDetail } from "./config.ts";
|
|
64
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
nativeResponseData,
|
|
66
|
+
NATIVE_RESPONSE_ENTRY_TYPE,
|
|
67
|
+
type NativeResponseAttempt,
|
|
68
|
+
} from "./native-history.ts";
|
|
65
69
|
import {
|
|
66
70
|
CODEX_NAMESPACED_TOOL_NAMES,
|
|
67
71
|
CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
|
|
@@ -78,6 +82,7 @@ import { formatProviderError } from "./provider-error.ts";
|
|
|
78
82
|
const CODEX_PROVIDER = "openai-codex";
|
|
79
83
|
const CODEX_API = "openai-codex-responses";
|
|
80
84
|
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
85
|
+
const PROVIDER_COMPACTION_BOUNDARY_STOP_REASON = "completed.end_turn_false.context_limit";
|
|
81
86
|
|
|
82
87
|
type ConfigResolver = (ctx: ExtensionContext) => CodexCompatConfig;
|
|
83
88
|
|
|
@@ -130,25 +135,30 @@ type CodexTerminalState = {
|
|
|
130
135
|
|
|
131
136
|
type CodexAttemptCapture = {
|
|
132
137
|
streamedItems: ResponsesItem[];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
terminalItems?: ResponsesItem[];
|
|
138
|
+
streamedToolCallIndexes: Set<number>;
|
|
139
|
+
streamedCompletedToolCallIndexes: Set<number>;
|
|
136
140
|
};
|
|
137
141
|
|
|
138
142
|
type CodexToolCallAssessment = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
completedCount: number;
|
|
144
|
+
discardedPartialCount: number;
|
|
145
|
+
hasCompletedCalls: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type CodexPostToolDisposition = {
|
|
149
|
+
callIds: string[];
|
|
150
|
+
response?: JsonRecord;
|
|
151
|
+
retryAttempt: number;
|
|
152
|
+
sessionId?: string;
|
|
153
|
+
terminalType: "response.incomplete" | "response.failed";
|
|
154
|
+
turnId: string;
|
|
155
|
+
type: "error" | "retry";
|
|
144
156
|
};
|
|
145
157
|
|
|
146
158
|
type CodexResponseDecision =
|
|
147
159
|
| "continue_no_tools"
|
|
148
|
-
| "preserve_terminal_error"
|
|
149
|
-
| "reject_terminal_stream_mismatch"
|
|
150
160
|
| "retry_original_input"
|
|
151
|
-
| "
|
|
161
|
+
| "return_compaction_boundary"
|
|
152
162
|
| "return_terminal"
|
|
153
163
|
| "return_tool_use";
|
|
154
164
|
|
|
@@ -217,62 +227,22 @@ function isToolCallItem(item: ResponsesItem): boolean {
|
|
|
217
227
|
return item.type === "function_call" || item.type === "custom_tool_call";
|
|
218
228
|
}
|
|
219
229
|
|
|
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";
|
|
230
|
+
function eventOutputIndex(event: JsonRecord): number {
|
|
231
|
+
return typeof event["output_index"] === "number" ? event["output_index"] : 0;
|
|
253
232
|
}
|
|
254
233
|
|
|
255
234
|
function assessAttemptToolCalls(
|
|
256
235
|
items: readonly ResponsesItem[],
|
|
257
236
|
capture: CodexAttemptCapture,
|
|
258
|
-
terminalType: CodexTerminalState["type"],
|
|
259
237
|
): CodexToolCallAssessment {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
(key) => !authoritativeKeys.has(key),
|
|
238
|
+
const completedCount = items.filter(isToolCallItem).length;
|
|
239
|
+
const discardedPartialCount = [...capture.streamedToolCallIndexes].filter(
|
|
240
|
+
(index) => !capture.streamedCompletedToolCallIndexes.has(index),
|
|
264
241
|
).length;
|
|
265
|
-
const hasToolCalls = calls.length > 0 || capture.streamedToolCallKeys.size > 0;
|
|
266
242
|
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,
|
|
243
|
+
completedCount,
|
|
244
|
+
discardedPartialCount,
|
|
245
|
+
hasCompletedCalls: completedCount > 0,
|
|
276
246
|
};
|
|
277
247
|
}
|
|
278
248
|
|
|
@@ -291,6 +261,7 @@ function responseDecisionDiagnostic(options: {
|
|
|
291
261
|
capture: CodexAttemptCapture;
|
|
292
262
|
decision: CodexResponseDecision;
|
|
293
263
|
incompleteReason?: string;
|
|
264
|
+
postToolDisposition?: "continue" | "error" | "retry";
|
|
294
265
|
terminalState: CodexTerminalState;
|
|
295
266
|
toolCalls: CodexToolCallAssessment;
|
|
296
267
|
}): AssistantMessageDiagnostic | undefined {
|
|
@@ -302,8 +273,7 @@ function responseDecisionDiagnostic(options: {
|
|
|
302
273
|
options.attempt > 1 ||
|
|
303
274
|
options.terminalState.type !== "response.completed" ||
|
|
304
275
|
endTurn === false ||
|
|
305
|
-
options.
|
|
306
|
-
options.toolCalls.omittedStreamedCount > 0;
|
|
276
|
+
options.capture.streamedToolCallIndexes.size > 0;
|
|
307
277
|
if (!nontrivial) return undefined;
|
|
308
278
|
|
|
309
279
|
return {
|
|
@@ -314,14 +284,12 @@ function responseDecisionDiagnostic(options: {
|
|
|
314
284
|
terminalType: options.terminalState.type ?? "missing",
|
|
315
285
|
...(options.incompleteReason ? { incompleteReason: options.incompleteReason } : {}),
|
|
316
286
|
...(endTurn === undefined ? {} : { endTurn }),
|
|
317
|
-
itemSource: options.capture.terminalItems ? "terminal" : "stream-fallback",
|
|
318
287
|
outputItemTypes: outputItemTypeCounts(options.attemptItems),
|
|
319
|
-
streamedCallsStarted: options.capture.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
allCallsComplete: options.toolCalls.allComplete,
|
|
288
|
+
streamedCallsStarted: options.capture.streamedToolCallIndexes.size,
|
|
289
|
+
streamedCallsDone: options.capture.streamedCompletedToolCallIndexes.size,
|
|
290
|
+
returnedCalls: options.toolCalls.completedCount,
|
|
291
|
+
discardedPartialCalls: options.toolCalls.discardedPartialCount,
|
|
292
|
+
...(options.postToolDisposition ? { postToolDisposition: options.postToolDisposition } : {}),
|
|
325
293
|
decision: options.decision,
|
|
326
294
|
},
|
|
327
295
|
};
|
|
@@ -340,28 +308,17 @@ function captureRawEvents(
|
|
|
340
308
|
isResponsesItem(event.item) &&
|
|
341
309
|
isToolCallItem(event.item)
|
|
342
310
|
) {
|
|
343
|
-
capture.
|
|
311
|
+
capture.streamedToolCallIndexes.add(eventOutputIndex(event));
|
|
344
312
|
}
|
|
345
313
|
if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
|
|
346
314
|
const item = structuredClone(event.item);
|
|
347
315
|
capture.streamedItems.push(item);
|
|
348
316
|
if (isToolCallItem(item)) {
|
|
349
|
-
const
|
|
350
|
-
capture.
|
|
351
|
-
capture.
|
|
317
|
+
const index = eventOutputIndex(event);
|
|
318
|
+
capture.streamedToolCallIndexes.add(index);
|
|
319
|
+
capture.streamedCompletedToolCallIndexes.add(index);
|
|
352
320
|
}
|
|
353
321
|
}
|
|
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
322
|
if (
|
|
366
323
|
terminalState &&
|
|
367
324
|
(event.type === "response.completed" ||
|
|
@@ -449,6 +406,22 @@ function accumulateUsage(previous: Usage, current: Usage): Usage {
|
|
|
449
406
|
};
|
|
450
407
|
}
|
|
451
408
|
|
|
409
|
+
function reachedProviderCompactionThreshold(
|
|
410
|
+
scope: RuntimeScope | undefined,
|
|
411
|
+
usage: Usage,
|
|
412
|
+
model: Model<any>,
|
|
413
|
+
): boolean {
|
|
414
|
+
const threshold = scope?.config.autoCompactAtPercent;
|
|
415
|
+
if (threshold === undefined || model.contextWindow <= 0 || usage.output >= model.maxTokens) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const contextTokens =
|
|
419
|
+
usage.totalTokens > 0
|
|
420
|
+
? usage.totalTokens
|
|
421
|
+
: usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
422
|
+
return contextTokens > 0 && (contextTokens / model.contextWindow) * 100 >= threshold;
|
|
423
|
+
}
|
|
424
|
+
|
|
452
425
|
function retryableResponseFailure(response: JsonRecord | undefined): boolean {
|
|
453
426
|
const error = isObject(response?.["error"]) ? response["error"] : undefined;
|
|
454
427
|
const code = typeof error?.["code"] === "string" ? error["code"].toLowerCase() : "";
|
|
@@ -462,6 +435,64 @@ function retryableResponseFailure(response: JsonRecord | undefined): boolean {
|
|
|
462
435
|
);
|
|
463
436
|
}
|
|
464
437
|
|
|
438
|
+
function terminalReason(terminalState: CodexTerminalState): string | undefined {
|
|
439
|
+
if (terminalState.type === "response.failed") {
|
|
440
|
+
const error = isObject(terminalState.response?.["error"])
|
|
441
|
+
? terminalState.response["error"]
|
|
442
|
+
: undefined;
|
|
443
|
+
return typeof error?.["code"] === "string" ? error["code"] : undefined;
|
|
444
|
+
}
|
|
445
|
+
if (terminalState.type === "response.incomplete") {
|
|
446
|
+
const details = isObject(terminalState.response?.["incomplete_details"])
|
|
447
|
+
? terminalState.response["incomplete_details"]
|
|
448
|
+
: undefined;
|
|
449
|
+
return typeof details?.["reason"] === "string" ? details["reason"] : undefined;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function terminalErrorMessage(disposition: CodexPostToolDisposition): string {
|
|
455
|
+
if (disposition.terminalType === "response.failed") {
|
|
456
|
+
const error = isObject(disposition.response?.["error"])
|
|
457
|
+
? disposition.response["error"]
|
|
458
|
+
: undefined;
|
|
459
|
+
return typeof error?.["message"] === "string" ? error["message"] : "Codex response failed";
|
|
460
|
+
}
|
|
461
|
+
const details = isObject(disposition.response?.["incomplete_details"])
|
|
462
|
+
? disposition.response["incomplete_details"]
|
|
463
|
+
: undefined;
|
|
464
|
+
const reason = typeof details?.["reason"] === "string" ? details["reason"] : undefined;
|
|
465
|
+
return reason
|
|
466
|
+
? `Response incomplete: ${reason}`
|
|
467
|
+
: "Response incomplete without a provider reason";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function completedAttemptToolCallIds(
|
|
471
|
+
message: AssistantMessage,
|
|
472
|
+
attempt: CodexStreamAttemptState,
|
|
473
|
+
): string[] {
|
|
474
|
+
return [...attempt.completedContentIndexes]
|
|
475
|
+
.sort((left, right) => left - right)
|
|
476
|
+
.flatMap((index) => {
|
|
477
|
+
const block = message.content[index];
|
|
478
|
+
return block?.type === "toolCall" ? [block.id] : [];
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function assertLinkedToolOutputs(context: Context, disposition: CodexPostToolDisposition): void {
|
|
483
|
+
const outputIds = new Set(
|
|
484
|
+
context.messages.flatMap((message) =>
|
|
485
|
+
message.role === "toolResult" ? [message.toolCallId] : [],
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
const missing = disposition.callIds.filter((callId) => !outputIds.has(callId));
|
|
489
|
+
if (missing.length > 0) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Codex cannot process the ${disposition.terminalType} response until Pi records tool output for: ${missing.join(", ")}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
465
496
|
function responseRetryDelayMs(baseDelayMs: number, attempt: number): number {
|
|
466
497
|
if (baseDelayMs <= 0) return 0;
|
|
467
498
|
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
@@ -598,6 +629,7 @@ export class CodexProviderRuntime {
|
|
|
598
629
|
private readonly prewarmedTemplates = new Set<string>();
|
|
599
630
|
private readonly requestTails = new Map<string, Promise<void>>();
|
|
600
631
|
private readonly activeAgentTurns = new Map<string, ActiveAgentTurn>();
|
|
632
|
+
private readonly postToolDispositions = new Map<string, CodexPostToolDisposition>();
|
|
601
633
|
private readonly windowNumbers = new Map<string, number>();
|
|
602
634
|
private readonly activeThreadIds = new Map<string, string>();
|
|
603
635
|
private readonly pi: ExtensionAPI;
|
|
@@ -646,7 +678,9 @@ export class CodexProviderRuntime {
|
|
|
646
678
|
}
|
|
647
679
|
|
|
648
680
|
beginAgentTurn(ctx: ExtensionContext): void {
|
|
649
|
-
|
|
681
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
682
|
+
this.clearPostToolDispositions((disposition) => disposition.sessionId === sessionId);
|
|
683
|
+
this.activeAgentTurns.set(sessionId, {
|
|
650
684
|
turnId: uuidv7(),
|
|
651
685
|
startedAtUnixMs: Date.now(),
|
|
652
686
|
turnState: new CodexTurnState(),
|
|
@@ -654,7 +688,12 @@ export class CodexProviderRuntime {
|
|
|
654
688
|
}
|
|
655
689
|
|
|
656
690
|
endAgentTurn(ctx: ExtensionContext): void {
|
|
657
|
-
|
|
691
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
692
|
+
const agentTurn = this.activeAgentTurns.get(sessionId);
|
|
693
|
+
if (agentTurn) {
|
|
694
|
+
this.clearPostToolDispositions((disposition) => disposition.turnId === agentTurn.turnId);
|
|
695
|
+
}
|
|
696
|
+
this.activeAgentTurns.delete(sessionId);
|
|
658
697
|
}
|
|
659
698
|
|
|
660
699
|
updateSessionConfig(sessionId: string, config: CodexCompatConfig): void {
|
|
@@ -679,6 +718,46 @@ export class CodexProviderRuntime {
|
|
|
679
718
|
);
|
|
680
719
|
}
|
|
681
720
|
|
|
721
|
+
private rememberPostToolDisposition(disposition: CodexPostToolDisposition): void {
|
|
722
|
+
const callIds = new Set(disposition.callIds);
|
|
723
|
+
if ([...callIds].some((callId) => this.postToolDispositions.has(callId))) {
|
|
724
|
+
throw new Error("Codex returned a tool call that already has a pending disposition.");
|
|
725
|
+
}
|
|
726
|
+
for (const callId of callIds) this.postToolDispositions.set(callId, disposition);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private findPostToolDisposition(context: Context): CodexPostToolDisposition | undefined {
|
|
730
|
+
const matches = new Set<CodexPostToolDisposition>();
|
|
731
|
+
for (const message of context.messages) {
|
|
732
|
+
if (message.role !== "assistant") continue;
|
|
733
|
+
for (const block of message.content) {
|
|
734
|
+
if (block.type !== "toolCall") continue;
|
|
735
|
+
const disposition = this.postToolDispositions.get(block.id);
|
|
736
|
+
if (disposition) matches.add(disposition);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (matches.size > 1) {
|
|
740
|
+
throw new Error("Codex context contains multiple pending post-tool dispositions.");
|
|
741
|
+
}
|
|
742
|
+
return matches.values().next().value;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private forgetPostToolDisposition(disposition: CodexPostToolDisposition): void {
|
|
746
|
+
for (const callId of disposition.callIds) {
|
|
747
|
+
if (this.postToolDispositions.get(callId) === disposition) {
|
|
748
|
+
this.postToolDispositions.delete(callId);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private clearPostToolDispositions(
|
|
754
|
+
shouldClear: (disposition: CodexPostToolDisposition) => boolean,
|
|
755
|
+
): void {
|
|
756
|
+
for (const [callId, disposition] of this.postToolDispositions) {
|
|
757
|
+
if (shouldClear(disposition)) this.postToolDispositions.delete(callId);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
682
761
|
private metadataIdentity(
|
|
683
762
|
metadataSessionId: string | undefined,
|
|
684
763
|
turn?: ActiveAgentTurn,
|
|
@@ -737,6 +816,7 @@ export class CodexProviderRuntime {
|
|
|
737
816
|
this.clearPrewarmState(sessionId);
|
|
738
817
|
this.requestTails.delete(sessionId);
|
|
739
818
|
this.activeAgentTurns.delete(sessionId);
|
|
819
|
+
this.clearPostToolDispositions((disposition) => disposition.sessionId === sessionId);
|
|
740
820
|
for (const key of this.windowNumbers.keys()) {
|
|
741
821
|
if (key.startsWith(`${sessionId}\0`)) this.windowNumbers.delete(key);
|
|
742
822
|
}
|
|
@@ -1161,6 +1241,7 @@ export class CodexProviderRuntime {
|
|
|
1161
1241
|
};
|
|
1162
1242
|
const runtimeSessionId = requestOptions.sessionId;
|
|
1163
1243
|
let releaseRequest = () => {};
|
|
1244
|
+
let registeredPostToolDisposition: CodexPostToolDisposition | undefined;
|
|
1164
1245
|
try {
|
|
1165
1246
|
const accountId = validateCodexAuthentication(model, requestOptions.apiKey);
|
|
1166
1247
|
releaseRequest = await this.acquireRequest(runtimeSessionId, requestOptions.signal);
|
|
@@ -1169,6 +1250,26 @@ export class CodexProviderRuntime {
|
|
|
1169
1250
|
requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
|
|
1170
1251
|
const agentTurn = this.agentTurn(runtimeSessionId);
|
|
1171
1252
|
const responsesLiteEnabled = this.responsesLiteEnabled(runtimeSessionId);
|
|
1253
|
+
let carriedResponseRetries = 0;
|
|
1254
|
+
const pendingPostToolDisposition = this.findPostToolDisposition(context);
|
|
1255
|
+
if (pendingPostToolDisposition) {
|
|
1256
|
+
assertLinkedToolOutputs(context, pendingPostToolDisposition);
|
|
1257
|
+
this.forgetPostToolDisposition(pendingPostToolDisposition);
|
|
1258
|
+
if (
|
|
1259
|
+
pendingPostToolDisposition.type === "error" ||
|
|
1260
|
+
pendingPostToolDisposition.retryAttempt > this.responseRetryPolicy.maxRetries
|
|
1261
|
+
) {
|
|
1262
|
+
throw new Error(terminalErrorMessage(pendingPostToolDisposition));
|
|
1263
|
+
}
|
|
1264
|
+
carriedResponseRetries = pendingPostToolDisposition.retryAttempt;
|
|
1265
|
+
await waitForResponseRetry(
|
|
1266
|
+
responseRetryDelayMs(
|
|
1267
|
+
this.responseRetryPolicy.baseDelayMs,
|
|
1268
|
+
pendingPostToolDisposition.retryAttempt,
|
|
1269
|
+
),
|
|
1270
|
+
requestOptions.signal,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1172
1273
|
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
1173
1274
|
context.tools,
|
|
1174
1275
|
(model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
|
|
@@ -1217,6 +1318,7 @@ export class CodexProviderRuntime {
|
|
|
1217
1318
|
);
|
|
1218
1319
|
|
|
1219
1320
|
const rawItems: ResponsesItem[] = [];
|
|
1321
|
+
const nativeAttempts: NativeResponseAttempt[] = [];
|
|
1220
1322
|
const prewarmDiagnostics: CodexTransportDiagnostic[] = [];
|
|
1221
1323
|
await this.maybePrewarm({
|
|
1222
1324
|
model,
|
|
@@ -1255,20 +1357,19 @@ export class CodexProviderRuntime {
|
|
|
1255
1357
|
},
|
|
1256
1358
|
};
|
|
1257
1359
|
let responseRequests = 0;
|
|
1258
|
-
let responseRetries =
|
|
1360
|
+
let responseRetries = carriedResponseRetries;
|
|
1259
1361
|
while (true) {
|
|
1260
1362
|
responseRequests += 1;
|
|
1261
1363
|
const attemptCapture: CodexAttemptCapture = {
|
|
1262
1364
|
streamedItems: [],
|
|
1263
|
-
|
|
1264
|
-
|
|
1365
|
+
streamedToolCallIndexes: new Set(),
|
|
1366
|
+
streamedCompletedToolCallIndexes: new Set(),
|
|
1265
1367
|
};
|
|
1266
1368
|
const terminalState: CodexTerminalState = {};
|
|
1267
1369
|
const attemptState: CodexStreamAttemptState = {
|
|
1268
1370
|
startedContentIndexes: new Set(),
|
|
1269
1371
|
completedContentIndexes: new Set(),
|
|
1270
1372
|
};
|
|
1271
|
-
const contentLengthBeforeAttempt = output.content.length;
|
|
1272
1373
|
const usageBeforeAttempt = structuredClone(output.usage);
|
|
1273
1374
|
output.usage = emptyUsage();
|
|
1274
1375
|
try {
|
|
@@ -1300,14 +1401,18 @@ export class CodexProviderRuntime {
|
|
|
1300
1401
|
throw error;
|
|
1301
1402
|
}
|
|
1302
1403
|
output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
const attemptItems = attemptCapture.
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1404
|
+
// `response.output_item.done` is Codex's item-level commit point. Terminal
|
|
1405
|
+
// response.output snapshots are deliberately ignored.
|
|
1406
|
+
const attemptItems = attemptCapture.streamedItems;
|
|
1407
|
+
if (terminalState.type) {
|
|
1408
|
+
const reason = terminalReason(terminalState);
|
|
1409
|
+
nativeAttempts.push({
|
|
1410
|
+
itemCount: attemptItems.length,
|
|
1411
|
+
terminalType: terminalState.type,
|
|
1412
|
+
...(reason ? { terminalReason: reason } : {}),
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
const toolCalls = assessAttemptToolCalls(attemptItems, attemptCapture);
|
|
1311
1416
|
const incompleteDetails = isObject(terminalState.response?.["incomplete_details"])
|
|
1312
1417
|
? terminalState.response["incomplete_details"]
|
|
1313
1418
|
: undefined;
|
|
@@ -1315,67 +1420,58 @@ export class CodexProviderRuntime {
|
|
|
1315
1420
|
typeof incompleteDetails?.["reason"] === "string"
|
|
1316
1421
|
? incompleteDetails["reason"]
|
|
1317
1422
|
: undefined;
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1423
|
+
const recordDecision = (
|
|
1424
|
+
decision: CodexResponseDecision,
|
|
1425
|
+
postToolDisposition?: "continue" | "error" | "retry",
|
|
1426
|
+
): void => {
|
|
1322
1427
|
const diagnostic = responseDecisionDiagnostic({
|
|
1323
1428
|
attempt: responseRequests,
|
|
1324
1429
|
attemptItems,
|
|
1325
1430
|
capture: attemptCapture,
|
|
1326
1431
|
decision,
|
|
1327
1432
|
...(incompleteReason ? { incompleteReason } : {}),
|
|
1433
|
+
...(postToolDisposition ? { postToolDisposition } : {}),
|
|
1328
1434
|
terminalState,
|
|
1329
1435
|
toolCalls,
|
|
1330
1436
|
});
|
|
1331
1437
|
if (diagnostic) output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
|
|
1332
1438
|
};
|
|
1333
1439
|
|
|
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;
|
|
1440
|
+
// Return each done tool call before processing an unsuccessful response
|
|
1441
|
+
// terminal. Pi will execute the completed subset; started-only siblings
|
|
1442
|
+
// are discarded and never enter provider history.
|
|
1443
|
+
if (toolCalls.hasCompletedCalls) {
|
|
1444
|
+
const callIds = completedAttemptToolCallIds(output, attemptState);
|
|
1445
|
+
if (callIds.length !== toolCalls.completedCount) {
|
|
1446
|
+
throw new Error("Codex completed tool-call items could not be mapped to Pi calls.");
|
|
1348
1447
|
}
|
|
1349
|
-
|
|
1350
|
-
output.content.splice(contentLengthBeforeAttempt);
|
|
1448
|
+
let postToolDisposition: "continue" | "error" | "retry" = "continue";
|
|
1351
1449
|
if (
|
|
1352
|
-
terminalState.type === "response.
|
|
1353
|
-
|
|
1354
|
-
responseRetries < this.responseRetryPolicy.maxRetries
|
|
1450
|
+
terminalState.type === "response.incomplete" ||
|
|
1451
|
+
terminalState.type === "response.failed"
|
|
1355
1452
|
) {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1453
|
+
const retryable =
|
|
1454
|
+
terminalState.type === "response.incomplete" ||
|
|
1455
|
+
retryableResponseFailure(terminalState.response);
|
|
1456
|
+
postToolDisposition = retryable ? "retry" : "error";
|
|
1457
|
+
registeredPostToolDisposition = {
|
|
1458
|
+
callIds,
|
|
1459
|
+
...(terminalState.response
|
|
1460
|
+
? { response: structuredClone(terminalState.response) }
|
|
1461
|
+
: {}),
|
|
1462
|
+
retryAttempt: retryable ? responseRetries + 1 : responseRetries,
|
|
1463
|
+
...(runtimeSessionId ? { sessionId: runtimeSessionId } : {}),
|
|
1464
|
+
terminalType: terminalState.type,
|
|
1465
|
+
turnId: agentTurn.turnId,
|
|
1466
|
+
type: postToolDisposition,
|
|
1467
|
+
};
|
|
1468
|
+
this.rememberPostToolDisposition(registeredPostToolDisposition);
|
|
1371
1469
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
: "preserve_terminal_error",
|
|
1378
|
-
);
|
|
1470
|
+
discardIncompleteAttemptContent(output, attemptState);
|
|
1471
|
+
rawItems.push(...attemptItems.map((item) => structuredClone(item)));
|
|
1472
|
+
output.stopReason = "toolUse";
|
|
1473
|
+
delete output.errorMessage;
|
|
1474
|
+
recordDecision("return_tool_use", postToolDisposition);
|
|
1379
1475
|
break;
|
|
1380
1476
|
}
|
|
1381
1477
|
|
|
@@ -1396,7 +1492,9 @@ export class CodexProviderRuntime {
|
|
|
1396
1492
|
output.stopReason = "pending";
|
|
1397
1493
|
delete output.errorMessage;
|
|
1398
1494
|
delete output.rawStopReason;
|
|
1399
|
-
recordDecision(
|
|
1495
|
+
recordDecision(
|
|
1496
|
+
attemptItems.length === 0 ? "retry_original_input" : "continue_no_tools",
|
|
1497
|
+
);
|
|
1400
1498
|
await waitForResponseRetry(
|
|
1401
1499
|
responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
|
|
1402
1500
|
requestOptions.signal,
|
|
@@ -1408,6 +1506,18 @@ export class CodexProviderRuntime {
|
|
|
1408
1506
|
terminalState.type === "response.completed" &&
|
|
1409
1507
|
terminalState.response?.["end_turn"] === false
|
|
1410
1508
|
) {
|
|
1509
|
+
const scope = runtimeSessionId ? this.scopes.get(runtimeSessionId) : undefined;
|
|
1510
|
+
if (reachedProviderCompactionThreshold(scope, output.usage, model)) {
|
|
1511
|
+
// Pi 0.84 has no provider event that can split one stream into two
|
|
1512
|
+
// assistant messages. Return the committed prefix as a recoverable
|
|
1513
|
+
// length boundary so Pi persists B1, runs native overflow compaction,
|
|
1514
|
+
// and continues from K without adding model-visible input.
|
|
1515
|
+
output.stopReason = "length";
|
|
1516
|
+
output.rawStopReason = PROVIDER_COMPACTION_BOUNDARY_STOP_REASON;
|
|
1517
|
+
delete output.errorMessage;
|
|
1518
|
+
recordDecision("return_compaction_boundary");
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1411
1521
|
if (!nextBody) {
|
|
1412
1522
|
throw new Error(
|
|
1413
1523
|
"Codex requested a follow-up response, but its completed output could not be appended to request history.",
|
|
@@ -1463,7 +1573,7 @@ export class CodexProviderRuntime {
|
|
|
1463
1573
|
if (!output.responseId) throw new Error("Codex response is missing a response id.");
|
|
1464
1574
|
this.pi.appendEntry(
|
|
1465
1575
|
NATIVE_RESPONSE_ENTRY_TYPE,
|
|
1466
|
-
nativeResponseData(model.id, output.responseId, rawItems),
|
|
1576
|
+
nativeResponseData(model.id, output.responseId, rawItems, nativeAttempts),
|
|
1467
1577
|
);
|
|
1468
1578
|
}
|
|
1469
1579
|
try {
|
|
@@ -1484,6 +1594,9 @@ export class CodexProviderRuntime {
|
|
|
1484
1594
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
1485
1595
|
stream.end();
|
|
1486
1596
|
} catch (error) {
|
|
1597
|
+
if (registeredPostToolDisposition) {
|
|
1598
|
+
this.forgetPostToolDisposition(registeredPostToolDisposition);
|
|
1599
|
+
}
|
|
1487
1600
|
clearStreamingScratchState(output);
|
|
1488
1601
|
output.stopReason = requestOptions.signal?.aborted ? "aborted" : "error";
|
|
1489
1602
|
output.errorMessage = formatProviderError(error);
|
|
@@ -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;
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
isResponsesItem,
|
|
15
15
|
type ResponsesItem,
|
|
16
16
|
} from "./codex-protocol.ts";
|
|
17
|
-
import { nativeResponseOverrides } from "./native-history.ts";
|
|
17
|
+
import { nativeCommittedPrefixBeforeOverflow, nativeResponseOverrides } from "./native-history.ts";
|
|
18
18
|
import {
|
|
19
19
|
CODEX_NAMESPACED_TOOL_NAMES,
|
|
20
20
|
CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
|
|
@@ -279,10 +279,11 @@ export function providerHistory(options: {
|
|
|
279
279
|
allTools: readonly ToolInfo[];
|
|
280
280
|
grammarToolInputProperties?: GrammarToolInputProperties;
|
|
281
281
|
imageDetail?: ImageDetail;
|
|
282
|
-
|
|
282
|
+
recoverLatestOverflowPrefix?: boolean;
|
|
283
283
|
}): ResponsesItem[] {
|
|
284
284
|
const branch = [...options.branch];
|
|
285
|
-
|
|
285
|
+
let recoveredPrefix: ResponsesItem[] = [];
|
|
286
|
+
if (options.recoverLatestOverflowPrefix) {
|
|
286
287
|
const index = branch.findLastIndex(
|
|
287
288
|
(entry) => entry.type === "message" && entry.message.role === "assistant",
|
|
288
289
|
);
|
|
@@ -292,6 +293,14 @@ export function providerHistory(options: {
|
|
|
292
293
|
entry.message.role === "assistant" &&
|
|
293
294
|
(entry.message.stopReason === "error" || entry.message.stopReason === "aborted")
|
|
294
295
|
) {
|
|
296
|
+
if (entry.message.stopReason === "error" && entry.message.responseId) {
|
|
297
|
+
recoveredPrefix =
|
|
298
|
+
nativeCommittedPrefixBeforeOverflow(
|
|
299
|
+
branch,
|
|
300
|
+
options.wireModel.id,
|
|
301
|
+
entry.message.responseId,
|
|
302
|
+
) ?? [];
|
|
303
|
+
}
|
|
295
304
|
branch.splice(index, 1);
|
|
296
305
|
}
|
|
297
306
|
}
|
|
@@ -317,16 +326,20 @@ export function providerHistory(options: {
|
|
|
317
326
|
options.imageDetail,
|
|
318
327
|
nativeAssistantItems,
|
|
319
328
|
),
|
|
329
|
+
...recoveredPrefix,
|
|
320
330
|
];
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
const context = buildSessionContext(branch);
|
|
324
|
-
return
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
334
|
+
return [
|
|
335
|
+
...encodeMessages(
|
|
336
|
+
options.wireModel,
|
|
337
|
+
convertToLlm(context.messages),
|
|
338
|
+
options.allTools,
|
|
339
|
+
options.grammarToolInputProperties ?? new Map(),
|
|
340
|
+
options.imageDetail ?? "auto",
|
|
341
|
+
nativeAssistantItems,
|
|
342
|
+
),
|
|
343
|
+
...recoveredPrefix,
|
|
344
|
+
];
|
|
332
345
|
}
|
|
@@ -31,9 +31,9 @@ export interface CodexCompatConfig {
|
|
|
31
31
|
/** Expose the standalone Codex web-search namespace tool. */
|
|
32
32
|
webRun: boolean;
|
|
33
33
|
/**
|
|
34
|
-
* Compact at
|
|
35
|
-
* percentage.
|
|
36
|
-
*
|
|
34
|
+
* Compact at provider request boundaries when context usage reaches this
|
|
35
|
+
* percentage. Mid-response boundaries use Pi's bounded compact-and-continue
|
|
36
|
+
* lifecycle, so Pi auto-compaction must remain enabled.
|
|
37
37
|
*/
|
|
38
38
|
autoCompactAtPercent?: number;
|
|
39
39
|
webSearch: WebSearchMode;
|
|
@@ -3,6 +3,13 @@ import { isObject, isResponsesItem, type ResponsesItem } from "./codex-protocol.
|
|
|
3
3
|
|
|
4
4
|
export const NATIVE_RESPONSE_ENTRY_TYPE = "openai-codex-compat-native-response";
|
|
5
5
|
export const NATIVE_RESPONSE_FORMAT_VERSION = 1;
|
|
6
|
+
export const NATIVE_RESPONSE_ITEM_COMMIT = "response.output_item.done";
|
|
7
|
+
|
|
8
|
+
export type NativeResponseAttempt = {
|
|
9
|
+
itemCount: number;
|
|
10
|
+
terminalType: "response.completed" | "response.incomplete" | "response.failed";
|
|
11
|
+
terminalReason?: string;
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
export type NativeResponseData = {
|
|
8
15
|
kind: typeof NATIVE_RESPONSE_ENTRY_TYPE;
|
|
@@ -10,12 +17,15 @@ export type NativeResponseData = {
|
|
|
10
17
|
modelId: string;
|
|
11
18
|
responseId: string;
|
|
12
19
|
items: ResponsesItem[];
|
|
20
|
+
itemCommit?: typeof NATIVE_RESPONSE_ITEM_COMMIT;
|
|
21
|
+
attempts?: NativeResponseAttempt[];
|
|
13
22
|
};
|
|
14
23
|
|
|
15
24
|
export function nativeResponseData(
|
|
16
25
|
modelId: string,
|
|
17
26
|
responseId: string,
|
|
18
27
|
items: readonly ResponsesItem[],
|
|
28
|
+
attempts?: readonly NativeResponseAttempt[],
|
|
19
29
|
): NativeResponseData {
|
|
20
30
|
return {
|
|
21
31
|
kind: NATIVE_RESPONSE_ENTRY_TYPE,
|
|
@@ -23,6 +33,14 @@ export function nativeResponseData(
|
|
|
23
33
|
modelId,
|
|
24
34
|
responseId,
|
|
25
35
|
items: items.map((item) => structuredClone(item)),
|
|
36
|
+
itemCommit: NATIVE_RESPONSE_ITEM_COMMIT,
|
|
37
|
+
...(attempts
|
|
38
|
+
? {
|
|
39
|
+
attempts: attempts.map((attempt) => ({
|
|
40
|
+
...attempt,
|
|
41
|
+
})),
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
26
44
|
};
|
|
27
45
|
}
|
|
28
46
|
|
|
@@ -45,15 +63,111 @@ export function parseNativeResponse(value: unknown): NativeResponseData | undefi
|
|
|
45
63
|
}
|
|
46
64
|
if (items.length === 0) return undefined;
|
|
47
65
|
|
|
66
|
+
const rawItemCommit = value["itemCommit"];
|
|
67
|
+
if (rawItemCommit !== undefined && rawItemCommit !== NATIVE_RESPONSE_ITEM_COMMIT) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const rawAttempts = value["attempts"];
|
|
71
|
+
let attempts: NativeResponseAttempt[] | undefined;
|
|
72
|
+
if (rawAttempts !== undefined) {
|
|
73
|
+
if (!Array.isArray(rawAttempts)) return undefined;
|
|
74
|
+
attempts = [];
|
|
75
|
+
for (const rawAttempt of rawAttempts) {
|
|
76
|
+
if (
|
|
77
|
+
!isObject(rawAttempt) ||
|
|
78
|
+
!Number.isSafeInteger(rawAttempt["itemCount"]) ||
|
|
79
|
+
(rawAttempt["itemCount"] as number) < 0 ||
|
|
80
|
+
(rawAttempt["terminalType"] !== "response.completed" &&
|
|
81
|
+
rawAttempt["terminalType"] !== "response.incomplete" &&
|
|
82
|
+
rawAttempt["terminalType"] !== "response.failed") ||
|
|
83
|
+
(rawAttempt["terminalReason"] !== undefined &&
|
|
84
|
+
typeof rawAttempt["terminalReason"] !== "string")
|
|
85
|
+
) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
attempts.push({
|
|
89
|
+
itemCount: rawAttempt["itemCount"] as number,
|
|
90
|
+
terminalType: rawAttempt["terminalType"],
|
|
91
|
+
...(typeof rawAttempt["terminalReason"] === "string"
|
|
92
|
+
? { terminalReason: rawAttempt["terminalReason"] }
|
|
93
|
+
: {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
return {
|
|
49
99
|
kind: NATIVE_RESPONSE_ENTRY_TYPE,
|
|
50
100
|
version: NATIVE_RESPONSE_FORMAT_VERSION,
|
|
51
101
|
modelId: value.modelId,
|
|
52
102
|
responseId: value["responseId"],
|
|
53
103
|
items,
|
|
104
|
+
...(rawItemCommit === NATIVE_RESPONSE_ITEM_COMMIT
|
|
105
|
+
? { itemCommit: NATIVE_RESPONSE_ITEM_COMMIT }
|
|
106
|
+
: {}),
|
|
107
|
+
...(attempts ? { attempts } : {}),
|
|
54
108
|
};
|
|
55
109
|
}
|
|
56
110
|
|
|
111
|
+
function linkedToolCalls(items: readonly ResponsesItem[]): boolean {
|
|
112
|
+
const unresolved = new Set<string>();
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
115
|
+
if (typeof item["call_id"] !== "string" || unresolved.has(item["call_id"])) return false;
|
|
116
|
+
unresolved.add(item["call_id"]);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
120
|
+
if (typeof item["call_id"] !== "string" || !unresolved.delete(item["call_id"])) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return unresolved.size === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Recover only done items from attempts completed before a final context-overflow
|
|
130
|
+
* subrequest. Older native entries remain replayable but lack enough provenance
|
|
131
|
+
* for this recovery path.
|
|
132
|
+
*/
|
|
133
|
+
export function nativeCommittedPrefixBeforeOverflow(
|
|
134
|
+
branch: readonly SessionEntry[],
|
|
135
|
+
modelId: string,
|
|
136
|
+
responseId: string,
|
|
137
|
+
): ResponsesItem[] | undefined {
|
|
138
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
139
|
+
const entry = branch[index]!;
|
|
140
|
+
if (entry.type !== "custom" || entry.customType !== NATIVE_RESPONSE_ENTRY_TYPE) continue;
|
|
141
|
+
const parsed = parseNativeResponse(entry.data);
|
|
142
|
+
if (!parsed) {
|
|
143
|
+
throw new Error(`Codex native response entry ${entry.id} is corrupt.`);
|
|
144
|
+
}
|
|
145
|
+
if (parsed.modelId !== modelId || parsed.responseId !== responseId) continue;
|
|
146
|
+
if (parsed.itemCommit !== NATIVE_RESPONSE_ITEM_COMMIT || !parsed.attempts) return undefined;
|
|
147
|
+
if (parsed.attempts.length < 2) return undefined;
|
|
148
|
+
if (
|
|
149
|
+
parsed.attempts.reduce((total, attempt) => total + attempt.itemCount, 0) !==
|
|
150
|
+
parsed.items.length
|
|
151
|
+
) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const finalAttempt = parsed.attempts.at(-1);
|
|
156
|
+
if (
|
|
157
|
+
finalAttempt?.terminalType !== "response.failed" ||
|
|
158
|
+
finalAttempt.terminalReason?.toLowerCase() !== "context_length_exceeded"
|
|
159
|
+
) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const prefixLength = parsed.items.length - finalAttempt.itemCount;
|
|
163
|
+
if (prefixLength <= 0) return undefined;
|
|
164
|
+
const prefix = parsed.items.slice(0, prefixLength);
|
|
165
|
+
if (!linkedToolCalls(prefix)) return undefined;
|
|
166
|
+
return prefix.map((item) => structuredClone(item));
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
57
171
|
/** Load native assistant output overrides from the active Pi branch. */
|
|
58
172
|
export function nativeResponseOverrides(
|
|
59
173
|
branch: readonly SessionEntry[],
|
|
@@ -163,7 +163,7 @@ export default function registerRemoteCompaction(
|
|
|
163
163
|
allTools,
|
|
164
164
|
grammarToolInputProperties,
|
|
165
165
|
imageDetail: config.imageDetail,
|
|
166
|
-
|
|
166
|
+
recoverLatestOverflowPrefix: event.reason === "overflow" && event.willRetry,
|
|
167
167
|
});
|
|
168
168
|
const template =
|
|
169
169
|
matching?.payload ??
|