pi-openai-codex-compat 0.0.10-alpha.7 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.10 - 2026-09-20
6
+
5
7
  ### Added
6
8
 
9
+ - Add the `pi-apply-patch parse` command-line entry point, which parses a
10
+ Codex patch document from a file or standard input and prints its
11
+ operations as JSON without touching the filesystem.
12
+ - Support `gpt-6-astra` for opt-in Responses Lite requests and `pro` reasoning
13
+ mode, without changing either setting's default.
14
+
7
15
  - Add configurable Codex `exec_command` + `write_stdin` and `shell_command`
8
16
  command surfaces, defaulting to persistent unified exec and replacing only
9
17
  an active Pi `bash` tool. Command output follows Pi's 2,000-line/50-KiB tail
@@ -18,6 +26,11 @@
18
26
 
19
27
  ### Fixed
20
28
 
29
+ - Preserve current instructions, dynamic tools, native compaction, and resumed
30
+ history with Pi 0.86's system-message transcript. Refresh tool declarations
31
+ when compaction replaces history and keep forced prompts authoritative.
32
+ - Match Pi 0.86's reasoning Off mapping and persist transport diagnostics as
33
+ strict JSON. Include standalone usage entries in footer totals.
21
34
  - Show effective unified-exec yield durations beside accent-colored command
22
35
  tool titles, include explicit working directories, emphasize commands on
23
36
  their own separated rows, separate `apply_patch` results, and mute command
@@ -80,7 +93,7 @@
80
93
 
81
94
  ### Changed
82
95
 
83
- - Support Pi 0.85.x and update the development baseline to Pi 0.85.1.
96
+ - Require Pi `>=0.86.0 <0.87.0` and use Pi 0.86.0 as the development baseline.
84
97
  - Describe `shell_command`, `exec_command`, and `write_stdin` in Pi's tool list,
85
98
  guidelines, and detailed tool contracts using the actual resolved default
86
99
  shell. Document command working-directory and `PI_*` context behavior,
@@ -106,9 +119,10 @@
106
119
  stderr while retaining Pi's complete-output files.
107
120
  - Replace the multi-line startup configuration dump with a concise package and
108
121
  settings-command notice, and simplify the fallback settings-save error.
109
- - Align deferred tool loading with Pi 0.84.3, preferring message-anchored
110
- additional tools on capable Codex models while retaining tool-search and
111
- top-level fallbacks.
122
+ - Read dynamic tool declarations from Pi 0.86 system messages, preferring
123
+ message-anchored additional tools on capable Codex models. Use tool-search
124
+ pairs on other capable models and the complete top-level tool set when
125
+ additions are unsupported or tools are removed or redefined.
112
126
  - Simplify `apply_patch` for its queued single-writer operating model: retain
113
127
  Pi's mutation queue, remove the extension-local alias queue and
114
128
  preflight-to-execution drift machinery, use direct writes for in-place text
package/README.md CHANGED
@@ -13,7 +13,7 @@ OpenAI Codex compatibility for [Pi](https://github.com/earendil-works/pi-mono),
13
13
  - **Standalone web search**: exposes Pi's dotted `web.run` tool as a native Responses namespace and executes search and browsing through Codex `alpha/search`.
14
14
  - **Dedicated Codex tool UI**: renders command tools, `apply_patch`, `image_gen.imagegen`, and `web.run` on a shared configurable surface with compact summaries and `Ctrl+O` expansion.
15
15
  - **Hosted web-search fallback**: injects native `web_search` only when `web.run` is inactive, with cached, indexed, or live modes.
16
- - **Native request controls**: configures Responses API text verbosity, reasoning summaries, and GPT-5.6 standard/pro reasoning mode.
16
+ - **Native request controls**: configures Responses API text verbosity, reasoning summaries, and standard/pro reasoning mode on supported models.
17
17
  - **Session-local settings pane**: `/codex-settings` changes every compatibility setting for the current session; `Enter` persists and closes, `Escape` discards unsaved changes and closes, and `Ctrl+S` persists without closing.
18
18
  - **Session-aware footer**: shows the current Pi session ID on the first line and appends non-default Codex request modes to the model side of Pi's normal second line.
19
19
 
@@ -22,7 +22,7 @@ Pi provides the Codex OAuth flow and model catalog. At session start, this packa
22
22
  ## Requirements
23
23
 
24
24
  - Node.js 22.19 or newer
25
- - Pi `>=0.84.0 <0.86.0`
25
+ - Pi `>=0.86.0 <0.87.0`
26
26
  - An OpenAI Codex login in Pi
27
27
 
28
28
  Authenticate through Pi if needed:
@@ -37,21 +37,21 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
37
37
 
38
38
  ### Configurable defaults that differ from Codex
39
39
 
40
- | Area | This package by default | Official Codex | Configuration |
41
- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | 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`. |
43
- | 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. |
44
- | 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. |
45
- | 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`. |
46
- | 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. |
47
- | Command tools | Replaces an active Pi `bash` tool with `exec_command` and `write_stdin`. | Chooses unified exec or legacy shell from model metadata, platform, execution environment, and runtime capabilities. | `shellTool`: `unified_exec` or `shell_command`. |
48
- | `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. |
49
- | `apply_patch` diagnostics capture | Disabled; no separate request or filesystem snapshot artifacts are retained. | Codex owns its rollout diagnostics rather than writing this package's artifact format. | `applyPatchDiagnostics`: boolean. |
50
- | 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`. |
51
- | 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. |
52
- | Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
53
- | Responses Lite | Disabled; supported GPT-5.6 models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
54
- | 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`. |
40
+ | Area | This package by default | Official Codex | Configuration |
41
+ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | 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`. |
43
+ | 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. |
44
+ | 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. |
45
+ | 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`. |
46
+ | 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. |
47
+ | Command tools | Replaces an active Pi `bash` tool with `exec_command` and `write_stdin`. | Chooses unified exec or legacy shell from model metadata, platform, execution environment, and runtime capabilities. | `shellTool`: `unified_exec` or `shell_command`. |
48
+ | `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. |
49
+ | `apply_patch` diagnostics capture | Disabled; no separate request or filesystem snapshot artifacts are retained. | Codex owns its rollout diagnostics rather than writing this package's artifact format. | `applyPatchDiagnostics`: boolean. |
50
+ | 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`. |
51
+ | 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. |
52
+ | Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
53
+ | Responses Lite | Disabled; supported models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
54
+ | Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries; omits the default 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`. |
55
55
 
56
56
  `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.
57
57
 
@@ -62,7 +62,8 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
62
62
  | Session storage | Pi remains the canonical session owner. Opaque Codex compaction checkpoints are stored in Pi `compaction` entries, and otherwise lossy Responses output is stored in sparse custom native-response entries. Official Codex owns a rollout/thread store directly. |
63
63
  | Branching | Checkpoints and native response overrides follow Pi's active session branch. Official Codex uses its own thread, turn, rollback, fork, and context-window lineage. |
64
64
  | Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
65
- | 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. |
65
+ | System instructions | Replay Pi's system-message sections into the complete current prompt. Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Do not replay older Pi system-prompt text alongside that current prompt. `/reload`, forced prompts, compaction snapshots, and resumed sessions use the current state without rewriting old checkpoints. |
66
+ | Dynamic tool declarations | Replay additive `toolsAdded` system messages in place as `additional_tools` or tool-search pairs on capable models. Tool removal or redefinition uses the complete current top-level tool set instead. Compaction rebases declarations so tools remain available after their history is replaced. |
66
67
  | 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. |
67
68
  | 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. |
68
69
  | 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. |
@@ -198,7 +199,7 @@ Defaults:
198
199
  | Setting | Values | Default | Behavior |
199
200
  | ----------------------- | ---------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
201
  | `fastMode` | boolean | `false` | Adds `service_tier: "priority"` to requests while retaining the current `openai-codex` provider and model. |
201
- | `responsesLite` | boolean | `false` | Uses Codex's Responses Lite input envelope on supported GPT-5.6 models when enabled. By default, those models use ordinary Responses instructions and tools. |
202
+ | `responsesLite` | boolean | `false` | Uses Codex's Responses Lite input envelope on supported models when enabled. By default, those models use ordinary Responses instructions and tools. |
202
203
  | `toolBackground` | `subtle`, `status`, `none` | `subtle` | Controls the shared self-rendered background for command tools, `apply_patch`, `image_gen.imagegen`, and `web.run`. `status` uses Pi's pending/success/error backgrounds; `none` keeps the custom layout transparent. |
203
204
  | `shellTool` | `unified_exec`, `shell_command` | `unified_exec` | Selects the command surface on `openai-codex` models. The selected Codex command surface replaces Pi `bash` only when `bash` was active. |
204
205
  | `applyPatch` | boolean | `true` | On selected `openai-codex` models, uses the extension's `apply_patch` tool instead of Pi's active `edit` and `write` tools. Other providers always use their normal Pi tool set. |
@@ -211,7 +212,12 @@ Defaults:
211
212
  | `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. |
212
213
  | `textVerbosity` | `low`, `medium`, `high` | `low` | Sets Responses API `text.verbosity`. |
213
214
  | `reasoningSummary` | `auto`, `concise`, `detailed`, `off` | `auto` | Sets `reasoning.summary` when reasoning is enabled; `off` omits the summary parameter. |
214
- | `reasoningMode` | `standard`, `pro` | `standard` | Controls GPT-5.6 execution mode independently of Pi's reasoning-effort control. The default omits `reasoning.mode`; `pro` sends `reasoning.mode: "pro"`. |
215
+ | `reasoningMode` | `standard`, `pro` | `standard` | Controls supported models' execution mode independently of Pi's reasoning-effort control. The default omits `reasoning.mode`; `pro` sends `reasoning.mode: "pro"`. |
216
+
217
+ Responses Lite supports exactly `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
218
+ and `gpt-6-astra`. Pro reasoning mode supports `gpt-5.6`, `gpt-5.6-*`, and
219
+ exactly `gpt-6-astra`. Both controls remain opt-in; other GPT-6 model IDs are
220
+ not included.
215
221
 
216
222
  Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from starting. The settings pane never writes on ordinary changes, refuses to overwrite invalid JSON when `Enter` or `Ctrl+S` attempts to save, and retains unknown keys when saving. Project configuration is read only when the project is trusted.
217
223
 
@@ -306,6 +312,12 @@ permissions.
306
312
 
307
313
  ## Native compaction
308
314
 
315
+ See the [Codex compaction approach review](CODEX_COMPACTION_APPROACH_REVIEW.md)
316
+ for a focused `0.153.4` source review, including experimental notes/history
317
+ recovery and unsummarized context resets, implementation options, and trade-offs.
318
+ That proposal does not change this package's runtime or its package-wide
319
+ compatibility baseline.
320
+
309
321
  The extension handles native compaction for `openai-codex`. It follows the Codex v2 flow:
310
322
 
311
323
  1. Send normal Responses history followed by `{ "type": "compaction_trigger" }`.
@@ -313,7 +325,7 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
313
325
  3. Retain approximately 64,000 tokens of recent user, developer, and system context.
314
326
  4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
315
327
 
316
- Ordinary responses and compaction use the same extension-managed SSE/WebSocket transport. Before the first WebSocket turn for a session and model, the provider performs a best-effort v2 `generate: false` prewarm of the static instruction/tool prefix, then generates the dynamic conversation input from its continuation. With `responsesLite: false`, supported GPT-5.6 models use the ordinary Responses envelope and receive their own ordinary-prefix prewarm.
328
+ Ordinary responses and compaction use the same extension-managed SSE/WebSocket transport. Before the first WebSocket turn for a session and model, the provider performs a best-effort v2 `generate: false` prewarm of the static instruction/tool prefix, then generates the dynamic conversation input from its continuation. With `responsesLite: false`, supported models use the ordinary Responses envelope and receive their own ordinary-prefix prewarm.
317
329
 
318
330
  Healthy session WebSockets remain available until the server closes them or Pi tears down the session. Retryable WebSocket failures before model-visible output receive up to five fresh-connection retries before the session switches to sticky SSE. Retryable SSE HTTP failures and dropped streams receive up to five same-request resampling attempts before model-visible output. Both transports use Codex-style exponential backoff with ±10% jitter and preserve the same prompt-cache, session, account, installation, and window identities. Server metadata is not considered model-visible output, so a routing-state-only response can still be retried safely. Transport failures after model-visible output fail closed rather than risk duplicate text or tool calls. Explicit retryable `response.failed`/`response.incomplete` protocol terminals instead return to the provider-owned sampling loop, which preserves completed output items as the next request's history.
319
331
 
@@ -467,6 +479,23 @@ cannot be inspected, it says that the final state was not verified.
467
479
  The complete feedback and rendering contract is documented in
468
480
  [`APPLY_PATCH_INSTRUCTION_FEEDBACK.md`](APPLY_PATCH_INSTRUCTION_FEEDBACK.md).
469
481
 
482
+ ### `pi-apply-patch` command line
483
+
484
+ The package installs a `pi-apply-patch` executable for use outside Pi sessions:
485
+
486
+ ```bash
487
+ pi-apply-patch parse patch.txt
488
+ cat patch.txt | pi-apply-patch parse
489
+ ```
490
+
491
+ `parse` runs the same parser as the tool and prints `{ operations, environmentId? }` as JSON.
492
+ Each operation is `add` with `content`, `delete`, or `update` with `chunks` and an optional
493
+ `moveTo`. It does not read or write any other file. Exit status is `0` when parsed, `1` for an
494
+ invalid patch, and `2` for a usage or read error.
495
+
496
+ The command line does not apply patches. Applying through a standalone process would bypass Pi's
497
+ file mutation queue and the tool-result contract, so applying stays inside the `apply_patch` tool.
498
+
470
499
  ## `image_gen.imagegen`
471
500
 
472
501
  The package registers the dotted Pi tool name `image_gen.imagegen` and serializes it as a native Responses API namespace:
@@ -537,8 +566,7 @@ Both namespace tools are accepted only from the fixed extension-owned allowlist.
537
566
  mise trust
538
567
  mise install
539
568
  npm install
540
- npm run check
541
- npm test
569
+ mise run check
542
570
  npm run pack:dry
543
571
  ```
544
572
 
@@ -12,7 +12,7 @@ Source: <https://github.com/openai/codex>
12
12
 
13
13
  ## Pi AI
14
14
 
15
- Selected OpenAI Codex transport, Responses stream processing, and history serialization methods are adapted from `@earendil-works/pi-ai`.
15
+ Selected OpenAI Codex transport, Responses stream processing, and history serialization methods are adapted from `@earendil-works/pi-ai`. History serialization follows the Pi 0.86.0 transcript contract.
16
16
 
17
17
  Pi AI, Copyright (c) 2025 Mario Zechner
18
18
 
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { text } from "node:stream/consumers";
3
+ import { runApplyPatchCli } from "../extensions/openai-codex-compat/apply-patch-cli.ts";
4
+
5
+ process.exitCode = await runApplyPatchCli(process.argv.slice(2), {
6
+ readStdin: () => text(process.stdin),
7
+ writeStdout: (content) => {
8
+ process.stdout.write(content);
9
+ },
10
+ writeStderr: (content) => {
11
+ process.stderr.write(content);
12
+ },
13
+ });
@@ -0,0 +1,65 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { ParsedPatch } from "./apply-patch-engine.ts";
3
+ import { ApplyPatchParseError, parsePatchDocument } from "./apply-patch-engine.ts";
4
+ import { errorMessage } from "./apply-patch-engine/apply-patch-engine-errors.ts";
5
+
6
+ export const APPLY_PATCH_CLI_NAME = "pi-apply-patch";
7
+
8
+ export const APPLY_PATCH_CLI_USAGE = `Usage: ${APPLY_PATCH_CLI_NAME} parse [<file>]
9
+
10
+ Parse a Codex apply_patch document and print its operations as JSON.
11
+ Reads <file>, or standard input when <file> is omitted or "-".
12
+
13
+ Exit status: 0 parsed, 1 invalid patch, 2 usage error.
14
+ `;
15
+
16
+ export type ApplyPatchCliIo = {
17
+ readStdin: () => Promise<string>;
18
+ writeStdout: (text: string) => void;
19
+ writeStderr: (text: string) => void;
20
+ };
21
+
22
+ export type ApplyPatchCliParseOutput = Pick<ParsedPatch, "operations" | "environmentId">;
23
+
24
+ export function applyPatchCliParseOutput(parsed: ParsedPatch): ApplyPatchCliParseOutput {
25
+ return parsed.environmentId === undefined
26
+ ? { operations: parsed.operations }
27
+ : { environmentId: parsed.environmentId, operations: parsed.operations };
28
+ }
29
+
30
+ export async function runApplyPatchCli(
31
+ argv: readonly string[],
32
+ io: ApplyPatchCliIo,
33
+ ): Promise<number> {
34
+ const [command, ...rest] = argv;
35
+ if (command === undefined || command === "--help" || command === "-h") {
36
+ io.writeStdout(APPLY_PATCH_CLI_USAGE);
37
+ return command === undefined ? 2 : 0;
38
+ }
39
+ if (command !== "parse" || rest.length > 1) {
40
+ io.writeStderr(APPLY_PATCH_CLI_USAGE);
41
+ return 2;
42
+ }
43
+
44
+ const source = rest[0];
45
+ let patch: string;
46
+ try {
47
+ patch =
48
+ source === undefined || source === "-"
49
+ ? await io.readStdin()
50
+ : await readFile(source, "utf8");
51
+ } catch (error) {
52
+ io.writeStderr(`${APPLY_PATCH_CLI_NAME}: cannot read patch: ${errorMessage(error)}\n`);
53
+ return 2;
54
+ }
55
+
56
+ try {
57
+ const output = applyPatchCliParseOutput(parsePatchDocument(patch));
58
+ io.writeStdout(`${JSON.stringify(output, null, 2)}\n`);
59
+ return 0;
60
+ } catch (error) {
61
+ if (!(error instanceof ApplyPatchParseError)) throw error;
62
+ io.writeStderr(`${APPLY_PATCH_CLI_NAME}: ${error.message}\n`);
63
+ return 1;
64
+ }
65
+ }
@@ -2,6 +2,8 @@ import { isBoolean, isNonNullObject, isNumber, isString } from "./value-contract
2
2
  import {
3
3
  calculateCost,
4
4
  type Api,
5
+ type JsonObject as PiJsonObject,
6
+ type JsonValue as PiJsonValue,
5
7
  type Model,
6
8
  type ProviderHeaders,
7
9
  type Usage,
@@ -69,7 +71,6 @@ export interface JsonRecord {
69
71
  name?: JsonValue;
70
72
  arguments?: JsonValue;
71
73
  toolCallId?: JsonValue;
72
- addedToolNames?: JsonValue;
73
74
  kind?: JsonValue;
74
75
  version?: JsonValue;
75
76
  modelId?: JsonValue;
@@ -105,6 +106,20 @@ export function requireJsonRecord(value: unknown, label = "value"): JsonRecord {
105
106
  return value;
106
107
  }
107
108
 
109
+ /** Omit optional wire fields before persisting diagnostics in Pi's strict JSON data. */
110
+ export function toPiJsonObject(value: JsonRecord): PiJsonObject {
111
+ const result: PiJsonObject = {};
112
+ for (const [key, entry] of Object.entries(value)) {
113
+ if (entry !== undefined) result[key] = toPiJsonValue(entry);
114
+ }
115
+ return result;
116
+ }
117
+
118
+ function toPiJsonValue(value: JsonValue): PiJsonValue {
119
+ if (Array.isArray(value)) return value.map(toPiJsonValue);
120
+ return isObject(value) ? toPiJsonObject(value) : value;
121
+ }
122
+
108
123
  export function optionalJsonRecord(value: unknown, label = "value"): JsonRecord | undefined {
109
124
  return value === undefined ? undefined : requireJsonRecord(value, label);
110
125
  }
@@ -1,43 +1,11 @@
1
1
  import type { SessionEntry, ToolInfo } from "@earendil-works/pi-coding-agent";
2
- import type { Api, Context, Model, Tool } from "@earendil-works/pi-ai";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
3
  import { providerHistory, type GrammarToolInputProperties } from "../compaction-checkpoint.ts";
4
4
  import type { ImageDetail } from "../config.ts";
5
5
  import { normalizeReplayItem, stableResponsesJson } from "../responses-replay.ts";
6
6
  import type { ResponsesInputItem, ResponsesOutputItem } from "../responses-item-schema.ts";
7
7
  import type { ResponsesItem as SerializedResponsesItem } from "../vendor/pi-ai/openai-responses-serialization.ts";
8
8
 
9
- export interface DeferredToolGroups {
10
- immediate: Tool[];
11
- deferred: Map<string, Tool>;
12
- }
13
-
14
- export function splitDeferredTools(context: Context, enabled: boolean): DeferredToolGroups {
15
- const unique = new Map((context.tools ?? []).map((tool) => [tool.name, tool]));
16
- if (!enabled) return { immediate: [...unique.values()], deferred: new Map() };
17
-
18
- const deferredNames = new Set<string>();
19
- const usedNames = new Set<string>();
20
- for (const message of context.messages) {
21
- if (message.role === "assistant") {
22
- for (const block of message.content) {
23
- if (block.type === "toolCall") usedNames.add(block.name);
24
- }
25
- } else if (message.role === "toolResult") {
26
- for (const name of message.addedToolNames ?? []) {
27
- if (!usedNames.has(name)) deferredNames.add(name);
28
- }
29
- }
30
- }
31
-
32
- const immediate: Tool[] = [];
33
- const deferred = new Map<string, Tool>();
34
- for (const [name, tool] of unique) {
35
- if (deferredNames.has(name)) deferred.set(name, tool);
36
- else immediate.push(tool);
37
- }
38
- return { immediate, deferred };
39
- }
40
-
41
9
  export function nativeOverrideRequired(
42
10
  rawItems: readonly ResponsesOutputItem[],
43
11
  canonicalItems: readonly SerializedResponsesItem[],
@@ -91,6 +59,7 @@ export function splitUnsampledUserInput(options: {
91
59
  allTools: options.allTools,
92
60
  grammarToolInputProperties: options.grammarToolInputProperties,
93
61
  imageDetail: options.imageDetail,
62
+ anchorsToolAdditions: false,
94
63
  });
95
64
  if (encoded.length === 0 || encoded.length > options.history.length) return { kind: "unsafe" };
96
65
 
@@ -4,6 +4,7 @@ import type {
4
4
  AssistantMessage,
5
5
  AssistantMessageDiagnostic,
6
6
  Context,
7
+ JsonObject,
7
8
  Model,
8
9
  OpenAICodexResponsesOptions,
9
10
  Usage,
@@ -57,7 +58,7 @@ export function assessAttemptToolCalls(
57
58
  }
58
59
 
59
60
  export interface OutputItemTypeCounts {
60
- [type: string]: number | undefined;
61
+ [type: string]: number;
61
62
  }
62
63
 
63
64
  export function outputItemTypeCounts(items: readonly ResponsesOutputItem[]): OutputItemTypeCounts {
@@ -91,7 +92,7 @@ export function responseDecisionDiagnostic(options: {
91
92
  options.capture.streamedToolCallIndexes.size > 0;
92
93
  if (!nontrivial) return undefined;
93
94
 
94
- const details: JsonRecord = {
95
+ const details: JsonObject = {
95
96
  attempt: options.attempt,
96
97
  terminalType: options.failureReason ?? options.terminalState?.type ?? "missing",
97
98
  outputItemTypes: outputItemTypeCounts(options.attemptItems),
@@ -5,6 +5,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  clampThinkingLevel,
7
7
  createAssistantMessageEventStream,
8
+ getCurrentSystemPrompt,
9
+ getDeclaredTools,
10
+ normalizeContext,
11
+ resolveTranscriptTools,
8
12
  type Api,
9
13
  type AssistantMessage,
10
14
  type AssistantMessageEventStream,
@@ -13,6 +17,7 @@ import {
13
17
  type OpenAICodexResponsesOptions,
14
18
  type Provider,
15
19
  type SimpleStreamOptions,
20
+ type TranscriptContext,
16
21
  type Usage,
17
22
  uuidv7,
18
23
  } from "@earendil-works/pi-ai";
@@ -26,7 +31,6 @@ import {
26
31
  providerHistory,
27
32
  remoteCompactionMarkerSummary,
28
33
  responsesCompatibility,
29
- responsesDeferredToolsMode,
30
34
  searchCheckpoint,
31
35
  type CheckpointData,
32
36
  type CompactionDecision,
@@ -38,6 +42,7 @@ import {
38
42
  isObject,
39
43
  remoteCompactionPayload,
40
44
  requireResponsesInputItems,
45
+ toPiJsonObject,
41
46
  withoutConversationInput,
42
47
  type JsonRecord,
43
48
  type JsonValue,
@@ -115,7 +120,6 @@ function codexStreamOptions(value: unknown): OpenAICodexResponsesOptions | undef
115
120
  }
116
121
  import {
117
122
  nativeOverrideRequired,
118
- splitDeferredTools,
119
123
  splitUnsampledUserInput,
120
124
  userEntryAfterLastSampled,
121
125
  } from "./codex-provider-history.ts";
@@ -156,7 +160,7 @@ type CodexCompactionResult = { checkpoint: CheckpointData; usage?: Usage };
156
160
 
157
161
  type BuildRequestBodyOptions = {
158
162
  model: Model<Api>;
159
- context: Context;
163
+ context: TranscriptContext;
160
164
  requestOptions: OpenAICodexResponsesOptions;
161
165
  runtimeSessionId: string | undefined;
162
166
  cacheSessionId: string | undefined;
@@ -488,19 +492,22 @@ export class CodexProviderRuntime {
488
492
 
489
493
  private wireHistory(
490
494
  model: Model<Api>,
491
- context: Context,
495
+ context: TranscriptContext,
492
496
  grammarToolInputProperties: GrammarToolInputProperties,
493
497
  sessionId: string | undefined,
498
+ anchorsToolAdditions: boolean,
494
499
  ): ResponsesInputItem[] {
495
500
  const scope = sessionId ? this.scopes.get(sessionId) : undefined;
501
+ const compat = responsesCompatibility(model.compat);
496
502
  if (!scope) {
497
- const compat = responsesCompatibility(model.compat);
498
- const deferredToolsMode = responsesDeferredToolsMode(compat);
499
503
  const nativeItems = new Map<string, ResponsesOutputItem[]>();
500
504
  const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
501
505
  includeSystemPrompt: false,
506
+ includeSystemUpdates: false,
507
+ supportsMidConvoSystemMessages: true,
508
+ supportsAdditionalTools: anchorsToolAdditions && (compat.supportsAdditionalTools ?? false),
509
+ supportsToolSearch: anchorsToolAdditions && (compat.supportsToolSearch ?? false),
502
510
  grammarToolInputProperties,
503
- deferredTools: splitDeferredTools(context, deferredToolsMode !== undefined).deferred,
504
511
  toolOptions: {
505
512
  strict: false,
506
513
  supportsStrictMode: compat.supportsStrictMode ?? true,
@@ -511,7 +518,6 @@ export class CodexProviderRuntime {
511
518
  toolResultImageDetail: "auto",
512
519
  nativeAssistantItems: nativeItems,
513
520
  };
514
- if (deferredToolsMode) serializationOptions.deferredToolsMode = deferredToolsMode;
515
521
  return requireResponsesInputItems(
516
522
  convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, serializationOptions),
517
523
  );
@@ -522,6 +528,7 @@ export class CodexProviderRuntime {
522
528
  allTools: this.pi.getAllTools(),
523
529
  grammarToolInputProperties,
524
530
  imageDetail: scope.config.imageDetail,
531
+ anchorsToolAdditions,
525
532
  });
526
533
  }
527
534
 
@@ -536,14 +543,39 @@ export class CodexProviderRuntime {
536
543
  turnId,
537
544
  } = options;
538
545
  const compat = responsesCompatibility(model.compat);
539
- const deferredToolsMode = responsesDeferredToolsMode(compat);
540
- const toolPlacement = splitDeferredTools(context, deferredToolsMode !== undefined);
546
+ const scope = runtimeSessionId ? this.scopes.get(runtimeSessionId) : undefined;
547
+ const threshold = scope?.config.autoCompactAtPercent;
548
+ // Percentage compaction drops historical tool additions. Rebase declarations
549
+ // before serialization so both the compaction and its continuation stay valid.
550
+ const compactionPending =
551
+ threshold !== undefined &&
552
+ scope?.contextPercent !== null &&
553
+ scope?.contextPercent !== undefined &&
554
+ scope.contextPercent >= threshold;
555
+ const toolPlacement = resolveTranscriptTools(
556
+ context.messages,
557
+ !compactionPending &&
558
+ ((compat.supportsAdditionalTools ?? false) || (compat.supportsToolSearch ?? false)),
559
+ );
560
+ // A forced prompt can project all tools into a single leading system message.
561
+ // Do not replay additions from the stored branch after that projection.
562
+ const anchorsToolAdditions =
563
+ toolPlacement.anchorsAdditions &&
564
+ context.messages
565
+ .slice(1)
566
+ .some((message) => message.role === "system" && (message.toolsAdded?.length ?? 0) > 0);
541
567
  let body: JsonRecord = {
542
568
  model: model.id,
543
569
  store: false,
544
570
  stream: true,
545
- instructions: context.systemPrompt || "You are a helpful assistant.",
546
- input: this.wireHistory(model, context, grammarToolInputProperties, runtimeSessionId),
571
+ instructions: getCurrentSystemPrompt(context.messages) || "You are a helpful assistant.",
572
+ input: this.wireHistory(
573
+ model,
574
+ context,
575
+ grammarToolInputProperties,
576
+ runtimeSessionId,
577
+ anchorsToolAdditions,
578
+ ),
547
579
  text: { verbosity: requestOptions.textVerbosity ?? "low" },
548
580
  include: ["reasoning.encrypted_content"],
549
581
  tool_choice: requestOptions.toolChoice ?? "auto",
@@ -562,8 +594,8 @@ export class CodexProviderRuntime {
562
594
  ),
563
595
  });
564
596
  if (requestOptions.serviceTier !== undefined) body.service_tier = requestOptions.serviceTier;
565
- if (toolPlacement.immediate.length > 0) {
566
- body.tools = convertResponsesTools(toolPlacement.immediate, {
597
+ if (toolPlacement.requestTools.length > 0) {
598
+ body.tools = convertResponsesTools(toolPlacement.requestTools, {
567
599
  strict: false,
568
600
  supportsStrictMode: compat.supportsStrictMode ?? true,
569
601
  supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools ?? false,
@@ -573,7 +605,9 @@ export class CodexProviderRuntime {
573
605
  if (requestOptions.reasoningEffort !== undefined) {
574
606
  const mapped =
575
607
  requestOptions.reasoningEffort === "none"
576
- ? (model.thinkingLevelMap?.off ?? "none")
608
+ ? model.thinkingLevelMap?.off === undefined
609
+ ? "none"
610
+ : model.thinkingLevelMap.off
577
611
  : (model.thinkingLevelMap?.[requestOptions.reasoningEffort] ??
578
612
  requestOptions.reasoningEffort);
579
613
  if (mapped !== null) {
@@ -582,6 +616,8 @@ export class CodexProviderRuntime {
582
616
  summary: requestOptions.reasoningSummary ?? "auto",
583
617
  };
584
618
  }
619
+ } else if (model.reasoning && model.thinkingLevelMap?.off !== null) {
620
+ body["reasoning"] = { effort: model.thinkingLevelMap?.off ?? "none" };
585
621
  }
586
622
  return body;
587
623
  }
@@ -682,7 +718,7 @@ export class CodexProviderRuntime {
682
718
 
683
719
  private async maybeCompactPercentage(
684
720
  model: Model<Api>,
685
- context: Context,
721
+ context: TranscriptContext,
686
722
  options: OpenAICodexResponsesOptions,
687
723
  body: JsonRecord,
688
724
  grammarToolInputProperties: GrammarToolInputProperties,
@@ -742,7 +778,7 @@ export class CodexProviderRuntime {
742
778
  template: withoutConversationInput(body),
743
779
  instructions: isString(body.instructions)
744
780
  ? body.instructions
745
- : context.systemPrompt || "You are a helpful assistant.",
781
+ : getCurrentSystemPrompt(context.messages) || "You are a helpful assistant.",
746
782
  grammarToolInputProperties,
747
783
  priority: scope.config.fastMode,
748
784
  compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
@@ -781,9 +817,10 @@ export class CodexProviderRuntime {
781
817
 
782
818
  stream(
783
819
  model: Model<Api>,
784
- context: Context,
820
+ input: Context,
785
821
  options?: OpenAICodexResponsesOptions,
786
822
  ): AssistantMessageEventStream {
823
+ const context = normalizeContext(input);
787
824
  const stream = createAssistantMessageEventStream();
788
825
  const requestOptions = transportOptions(options);
789
826
  const output: AssistantMessage = {
@@ -828,7 +865,7 @@ export class CodexProviderRuntime {
828
865
  );
829
866
  }
830
867
  const grammarToolInputProperties = createGrammarToolInputProperties(
831
- context.tools,
868
+ getDeclaredTools(context.messages),
832
869
  responsesCompatibility(model.compat).supportsOpenAIGrammarTools ?? false,
833
870
  );
834
871
  let body = this.buildRequestBody({
@@ -903,7 +940,12 @@ export class CodexProviderRuntime {
903
940
  turnState: agentTurn.turnState,
904
941
  cacheDiagnostics,
905
942
  });
906
- if (prewarmDiagnostics.length > 0) output.diagnostics = prewarmDiagnostics;
943
+ if (prewarmDiagnostics.length > 0) {
944
+ output.diagnostics = prewarmDiagnostics.map((diagnostic) => ({
945
+ ...diagnostic,
946
+ details: toPiJsonObject(diagnostic.details),
947
+ }));
948
+ }
907
949
  let continuationHandle: CodexContinuationHandle | undefined;
908
950
  let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
909
951
  let startEmitted = false;
@@ -926,7 +968,10 @@ export class CodexProviderRuntime {
926
968
  },
927
969
  onTransportStart: emitStart,
928
970
  onTransportDiagnostic(diagnostic: CodexTransportDiagnostic) {
929
- output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
971
+ output.diagnostics = [
972
+ ...(output.diagnostics ?? []),
973
+ { ...diagnostic, details: toPiJsonObject(diagnostic.details) },
974
+ ];
930
975
  },
931
976
  };
932
977
  let responseRequests = 0;
@@ -1153,12 +1198,9 @@ export class CodexProviderRuntime {
1153
1198
  const canonicalContext: Context = {
1154
1199
  messages: [output],
1155
1200
  };
1156
- if (context.tools) canonicalContext.tools = context.tools;
1157
- const deferredToolsMode = responsesDeferredToolsMode(compat);
1158
1201
  const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
1159
1202
  includeSystemPrompt: false,
1160
1203
  grammarToolInputProperties,
1161
- deferredTools: splitDeferredTools(context, deferredToolsMode !== undefined).deferred,
1162
1204
  toolOptions: {
1163
1205
  strict: false,
1164
1206
  supportsStrictMode: compat.supportsStrictMode ?? true,
@@ -1170,7 +1212,6 @@ export class CodexProviderRuntime {
1170
1212
  (runtimeSessionId ? this.scopes.get(runtimeSessionId)?.config.imageDetail : undefined) ??
1171
1213
  "auto",
1172
1214
  };
1173
- if (deferredToolsMode) serializationOptions.deferredToolsMode = deferredToolsMode;
1174
1215
  const canonicalItems = convertResponsesMessages(
1175
1216
  model,
1176
1217
  canonicalContext,
@@ -5,7 +5,7 @@ import type {
5
5
  } from "@earendil-works/pi-ai";
6
6
  import type { CodexCacheDiagnosticContext } from "../codex-cache-diagnostics.ts";
7
7
  import type { CodexRequestKind } from "../codex-metadata.ts";
8
- import type { JsonRecord } from "../codex-protocol.ts";
8
+ import type { JsonRecord, JsonValue } from "../codex-protocol.ts";
9
9
  import type { CodexTurnState } from "./codex-transport-turn-state.ts";
10
10
 
11
11
  export type CodexJsonRequestOptions = {
@@ -43,8 +43,8 @@ export type CodexContinuationHistoryMismatch = {
43
43
  index: number;
44
44
  baselineInputItems: number;
45
45
  currentInputItems: number;
46
- baselineItem?: unknown;
47
- currentItem?: unknown;
46
+ baselineItem?: JsonValue;
47
+ currentItem?: JsonValue;
48
48
  };
49
49
 
50
50
  export type CodexTransportRecoveryAttempt = {
@@ -75,11 +75,11 @@ export function cachedRequestBody(entry: CachedWebSocket, body: JsonRecord): Cac
75
75
  index: mismatchIndex,
76
76
  baselineInputItems: baseline.length,
77
77
  currentInputItems: currentInput.length,
78
- baselineItem: structuredClone(baseline[mismatchIndex]),
79
78
  };
80
- if (mismatchIndex < currentInput.length) {
81
- historyMismatch.currentItem = structuredClone(currentInput[mismatchIndex]);
82
- }
79
+ const baselineItem = baseline[mismatchIndex];
80
+ const currentItem = currentInput[mismatchIndex];
81
+ if (baselineItem !== undefined) historyMismatch.baselineItem = structuredClone(baselineItem);
82
+ if (currentItem !== undefined) historyMismatch.currentItem = structuredClone(currentItem);
83
83
  return {
84
84
  body,
85
85
  contextMode: "full",
@@ -7,7 +7,7 @@ import {
7
7
  type SessionEntry,
8
8
  type ToolInfo,
9
9
  } from "@earendil-works/pi-coding-agent";
10
- import type { Api, Message, Model, Tool } from "@earendil-works/pi-ai";
10
+ import type { Api, Message, Model, SystemMessage } from "@earendil-works/pi-ai";
11
11
  import { Value } from "typebox/value";
12
12
  import { APPLY_PATCH_LARK_GRAMMAR, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
13
13
  import { CODEX_TOOL_CALL_PROVIDERS } from "./codex-identifiers.ts";
@@ -32,10 +32,7 @@ import {
32
32
  type ResponsesOutputItem,
33
33
  } from "./responses-item-schema.ts";
34
34
  import type { ResponsesToolDefinition } from "./responses-tool-schema.ts";
35
- import {
36
- convertResponsesMessages,
37
- type DeferredToolsMode,
38
- } from "./vendor/pi-ai/openai-responses-serialization.ts";
35
+ import { convertResponsesMessages } from "./vendor/pi-ai/openai-responses-serialization.ts";
39
36
 
40
37
  export const CHECKPOINT_ENTRY_TYPE = "openai-codex-compat-remote-compaction";
41
38
  export const CHECKPOINT_FORMAT_VERSION = 1;
@@ -85,14 +82,6 @@ export function responsesCompatibility(value: unknown): ResponsesCompatibility {
85
82
  return compatibility;
86
83
  }
87
84
 
88
- export function responsesDeferredToolsMode(
89
- compatibility: ResponsesCompatibility,
90
- ): DeferredToolsMode | undefined {
91
- if (compatibility.supportsAdditionalTools) return "additional-tools";
92
- if (compatibility.supportsToolSearch) return "tool-search";
93
- return undefined;
94
- }
95
-
96
85
  function responsesToolParameters(tool: ToolInfo): JsonRecord {
97
86
  if (!isObject(tool.parameters)) {
98
87
  throw new Error(`Tool ${tool.name} must have JSON object parameters.`);
@@ -164,6 +153,7 @@ type EncodeMessagesOptions = {
164
153
  allTools: readonly ToolInfo[];
165
154
  grammarToolInputProperties: GrammarToolInputProperties;
166
155
  imageDetail: ImageDetail;
156
+ anchorsToolAdditions?: boolean;
167
157
  nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
168
158
  };
169
159
 
@@ -174,40 +164,24 @@ export type EncodeSessionEntriesOptions = {
174
164
  grammarToolInputProperties?: GrammarToolInputProperties;
175
165
  imageDetail?: ImageDetail;
176
166
  nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
167
+ initialSystemMessage?: SystemMessage;
168
+ anchorsToolAdditions?: boolean;
177
169
  };
178
170
 
179
- function asPiTool(tool: ToolInfo, grammarToolInputProperties: GrammarToolInputProperties): Tool {
180
- const piTool: Tool = {
181
- name: tool.name,
182
- description: tool.description,
183
- parameters: tool.parameters,
184
- };
185
- if (tool.name === APPLY_PATCH_TOOL_NAME && grammarToolInputProperties.has(tool.name)) {
186
- piTool.constrainedSampling = {
187
- type: "grammar",
188
- variants: { openai_lark: APPLY_PATCH_LARK_GRAMMAR },
189
- };
190
- }
191
- return piTool;
192
- }
193
-
194
171
  /** Encode Pi's canonical messages using Pi AI's OpenAI Responses serializer. */
195
172
  function encodeMessages(options: EncodeMessagesOptions): ResponsesInputItem[] {
196
- const {
197
- model,
198
- messages,
199
- allTools,
200
- grammarToolInputProperties,
201
- imageDetail,
202
- nativeAssistantItems,
203
- } = options;
204
- const tools = allTools.map((tool) => asPiTool(tool, grammarToolInputProperties));
173
+ const { model, messages, grammarToolInputProperties, imageDetail, nativeAssistantItems } =
174
+ options;
205
175
  const compat = responsesCompatibility(model.compat);
206
- const deferredToolsMode = responsesDeferredToolsMode(compat);
207
176
  const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
208
177
  includeSystemPrompt: false,
178
+ includeSystemUpdates: false,
179
+ supportsMidConvoSystemMessages: true,
180
+ supportsAdditionalTools:
181
+ options.anchorsToolAdditions !== false && (compat.supportsAdditionalTools ?? false),
182
+ supportsToolSearch:
183
+ options.anchorsToolAdditions !== false && (compat.supportsToolSearch ?? false),
209
184
  grammarToolInputProperties,
210
- deferredTools: new Map(tools.map((tool) => [tool.name, tool])),
211
185
  toolOptions: {
212
186
  strict: false,
213
187
  supportsStrictMode: compat?.supportsStrictMode ?? true,
@@ -217,17 +191,11 @@ function encodeMessages(options: EncodeMessagesOptions): ResponsesInputItem[] {
217
191
  textContentItemToolResultNames: CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
218
192
  toolResultImageDetail: imageDetail,
219
193
  };
220
- if (deferredToolsMode) serializationOptions.deferredToolsMode = deferredToolsMode;
221
194
  if (nativeAssistantItems) {
222
195
  serializationOptions.nativeAssistantItems = nativeAssistantItems;
223
196
  }
224
197
  return requireResponsesInputItems(
225
- convertResponsesMessages(
226
- model,
227
- { messages, tools },
228
- CODEX_TOOL_CALL_PROVIDERS,
229
- serializationOptions,
230
- ),
198
+ convertResponsesMessages(model, { messages }, CODEX_TOOL_CALL_PROVIDERS, serializationOptions),
231
199
  );
232
200
  }
233
201
 
@@ -243,11 +211,19 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
243
211
  const messages = entries.flatMap((entry) => sessionEntryToContextMessages(entry));
244
212
  const encodeOptions: EncodeMessagesOptions = {
245
213
  model,
246
- messages: convertToLlm(messages),
214
+ // A partial tail can start with a system update. Seed its prior state so
215
+ // the serializer does not mistake that update for the leading declaration.
216
+ messages: [
217
+ options.initialSystemMessage ?? { role: "system", content: "", timestamp: 0 },
218
+ ...convertToLlm(messages),
219
+ ],
247
220
  allTools,
248
221
  grammarToolInputProperties,
249
222
  imageDetail,
250
223
  };
224
+ if (options.anchorsToolAdditions !== undefined) {
225
+ encodeOptions.anchorsToolAdditions = options.anchorsToolAdditions;
226
+ }
251
227
  if (nativeAssistantItems) encodeOptions.nativeAssistantItems = nativeAssistantItems;
252
228
  return encodeMessages(encodeOptions);
253
229
  }
@@ -371,6 +347,7 @@ export function providerHistory(options: {
371
347
  grammarToolInputProperties?: GrammarToolInputProperties;
372
348
  imageDetail?: ImageDetail;
373
349
  recoverLatestOverflowPrefix?: boolean;
350
+ anchorsToolAdditions?: boolean;
374
351
  }): ResponsesInputItem[] {
375
352
  const branch = [...options.branch];
376
353
  let recoveredPrefix: ResponsesInputItem[] = [];
@@ -407,6 +384,9 @@ export function providerHistory(options: {
407
384
  `The latest Codex compaction checkpoint belongs to ${checkpoint.data.modelId}, not ${options.wireModel.id}.`,
408
385
  );
409
386
  }
387
+ const checkpointEntry = branch[checkpoint.entryIndex];
388
+ const initialSystemMessage =
389
+ checkpointEntry?.type === "compaction" ? checkpointEntry.systemMessage : undefined;
410
390
  return [
411
391
  ...checkpoint.data.history.map((item) => structuredClone(item)),
412
392
  ...encodeSessionEntries({
@@ -416,6 +396,10 @@ export function providerHistory(options: {
416
396
  grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
417
397
  imageDetail: options.imageDetail ?? "auto",
418
398
  nativeAssistantItems,
399
+ ...(initialSystemMessage ? { initialSystemMessage } : {}),
400
+ ...(options.anchorsToolAdditions === undefined
401
+ ? {}
402
+ : { anchorsToolAdditions: options.anchorsToolAdditions }),
419
403
  }),
420
404
  ...recoveredPrefix,
421
405
  ];
@@ -430,6 +414,9 @@ export function providerHistory(options: {
430
414
  grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
431
415
  imageDetail: options.imageDetail ?? "auto",
432
416
  nativeAssistantItems,
417
+ ...(options.anchorsToolAdditions === undefined
418
+ ? {}
419
+ : { anchorsToolAdditions: options.anchorsToolAdditions }),
433
420
  }),
434
421
  ...recoveredPrefix,
435
422
  ];
@@ -158,7 +158,10 @@ function aggregateFooterEntries(entries: readonly SessionEntry[]): SessionEntry[
158
158
  entry.message.usage
159
159
  ) {
160
160
  addUsage(precedingUsage, entry.message.usage);
161
- } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
161
+ } else if (
162
+ (entry.type === "usage" || entry.type === "branch_summary" || entry.type === "compaction") &&
163
+ entry.usage
164
+ ) {
162
165
  addUsage(precedingUsage, entry.usage);
163
166
  }
164
167
  }
@@ -249,17 +249,13 @@ export default function registerRemoteCompaction(
249
249
  grammarToolInputProperties,
250
250
  imageDetail: config.imageDetail,
251
251
  recoverLatestOverflowPrefix: event.reason === "overflow" && event.willRetry,
252
+ anchorsToolAdditions: false,
252
253
  });
253
- let template = matching?.payload;
254
- if (!template) {
255
- template = {};
256
- const tools = activeResponsesTools(
257
- allTools,
258
- pi.getActiveTools(),
259
- grammarToolInputProperties,
260
- );
261
- if (tools) template.tools = tools;
262
- }
254
+ // A cached turn template can contain only the initial declarations.
255
+ // Compaction rebases history, so use the complete current tool set.
256
+ const template: JsonRecord = { ...matching?.payload };
257
+ template.tools =
258
+ activeResponsesTools(allTools, pi.getActiveTools(), grammarToolInputProperties) ?? [];
263
259
  const requestOptions: OpenAICodexResponsesOptions = {
264
260
  ...matching?.requestOptions,
265
261
  apiKey: authentication.apiKey,
@@ -20,7 +20,7 @@ export function isCodexModel(model: Model<Api> | undefined): model is Model<type
20
20
  }
21
21
 
22
22
  export function supportsReasoningMode(modelId: string): boolean {
23
- return /^gpt-5\.6(?:-|$)/.test(modelId);
23
+ return modelId === "gpt-6-astra" || /^gpt-5\.6(?:-|$)/.test(modelId);
24
24
  }
25
25
 
26
26
  function isWebSearchTool(value: unknown): boolean {
@@ -9,6 +9,7 @@ const RESPONSES_LITE_MODELS: ReadonlySet<string> = new Set([
9
9
  "gpt-5.6-sol",
10
10
  "gpt-5.6-terra",
11
11
  "gpt-5.6-luna",
12
+ "gpt-6-astra",
12
13
  ]);
13
14
 
14
15
  function isHostedTool(tool: JsonRecord): boolean {
@@ -2,12 +2,20 @@
2
2
 
3
3
  This directory intentionally contains only the Pi AI methods needed to serialize checkpoint and native replay history for OpenAI's Responses API. It does not contain Pi AI's complete source dependency graph.
4
4
 
5
- [`openai-responses-serialization.ts`](openai-responses-serialization.ts) adapts the relevant methods from `@earendil-works/pi-ai@0.84.3`. Its header lists the upstream source files. Keep the behavioral equivalence test in [`test/pi-ai-serialization.test.ts`](../../../../test/pi-ai-serialization.test.ts) passing when updating the Pi dependencies.
6
-
7
- Pi AI's `deferredToolsMode` option selects the model-compatible dynamic tool
8
- history representation: message-anchored `additional_tools` or the
9
- `tool_search_call`/`tool_search_output` pair. Without either capability,
10
- callers must keep every tool in the request's top-level tool list.
5
+ [`openai-responses-serialization.ts`](openai-responses-serialization.ts) adapts the relevant methods from `@earendil-works/pi-ai@0.86.0`. Its header lists the upstream source files. Keep the wire-equivalence tests in [`test/pi-ai-serialization.test.ts`](../../../../test/pi-ai-serialization.test.ts) passing when updating the Pi dependencies.
6
+
7
+ Pi AI's transcript helpers replay system sections and tool declarations.
8
+ `supportsAdditionalTools` and `supportsToolSearch` select the dynamic tool
9
+ history representation for additive `toolsAdded` system messages:
10
+ message-anchored `additional_tools` or a `tool_search_call`/`tool_search_output`
11
+ pair. Tool removal, redefinition, or a model without either capability requires
12
+ the complete current top-level tool list.
13
+
14
+ Compat uses the extension-owned `includeSystemUpdates: false` option alongside
15
+ `includeSystemPrompt: false`. It sends the complete current prompt separately
16
+ and preserves only supported tool additions in system-message positions.
17
+ This keeps old prompt text from overriding current instructions after
18
+ compaction, forced-prompt projection, reload, or resume.
11
19
 
12
20
  The optional `namespacedToolNames`, `textContentItemToolResultNames`, and `toolResultImageDetail` paths are extension-owned additions. Without those options, serialization must continue to match Pi AI. `namespacedToolNames` groups only the fixed Codex allowlist into native Responses namespaces and replays namespace/member call identities. `textContentItemToolResultNames` preserves Codex tools such as `web.run` whose successful text output is transported as an `input_text` content-item array instead of Pi's usual plain string. `toolResultImageDetail` overrides the otherwise canonical `auto` detail used for image tool-result content.
13
21
 
@@ -19,10 +19,17 @@ import type {
19
19
  ToolCall,
20
20
  ToolResultMessage,
21
21
  } from "@earendil-works/pi-ai";
22
+ import {
23
+ getSystemMessageText,
24
+ normalizeContext,
25
+ renderSystemMessageUpdate,
26
+ resolveTranscript,
27
+ resolveTranscriptTools,
28
+ } from "@earendil-works/pi-ai";
22
29
 
23
30
  /**
24
31
  * Focused copies of the methods used to serialize Pi messages for OpenAI's
25
- * Responses API. Adapted from @earendil-works/pi-ai@0.84.3:
32
+ * Responses API. Adapted from @earendil-works/pi-ai@0.86.0:
26
33
  *
27
34
  * - src/api/openai-responses-shared.ts
28
35
  * - src/api/transform-messages.ts
@@ -36,7 +43,6 @@ import type {
36
43
  */
37
44
 
38
45
  export type ResponsesItem = JsonRecord;
39
- export type DeferredToolsMode = "additional-tools" | "tool-search";
40
46
  export type ToolResultImageDetail = "auto" | "low" | "high" | "original";
41
47
  type ToolResultOutput =
42
48
  | string
@@ -47,9 +53,12 @@ type ToolResultOutput =
47
53
 
48
54
  type ConvertResponsesMessagesOptions = {
49
55
  includeSystemPrompt?: boolean;
56
+ /** Compat sends the complete current prompt outside replay history. */
57
+ includeSystemUpdates?: boolean;
50
58
  grammarToolInputProperties?: ReadonlyMap<string, string>;
51
- deferredTools?: ReadonlyMap<string, Tool>;
52
- deferredToolsMode?: DeferredToolsMode;
59
+ supportsMidConvoSystemMessages?: boolean;
60
+ supportsAdditionalTools?: boolean;
61
+ supportsToolSearch?: boolean;
53
62
  toolOptions?: ConvertResponsesToolsOptions;
54
63
  nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesItem[]>;
55
64
  namespacedToolNames?: ReadonlySet<string>;
@@ -333,7 +342,7 @@ function transformMessages(
333
342
  const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
334
343
 
335
344
  const transformed = imageAwareMessages.map((message) => {
336
- if (message.role === "user") return message;
345
+ if (message.role === "user" || message.role === "system") return message;
337
346
  if (message.role === "toolResult") {
338
347
  const normalizedId = toolCallIdMap.get(message.toolCallId);
339
348
  return normalizedId && normalizedId !== message.toolCallId
@@ -484,8 +493,11 @@ export function convertResponsesMessages(
484
493
  allowedToolCallProviders: ReadonlySet<string>,
485
494
  options?: ConvertResponsesMessagesOptions,
486
495
  ): ResponsesItem[] {
496
+ const transcript = resolveTranscript(
497
+ normalizeContext(context),
498
+ options?.supportsMidConvoSystemMessages,
499
+ );
487
500
  const messages: ResponsesItem[] = [];
488
- const loadedToolNames = new Set<string>();
489
501
  const normalizeIdPart = (part: string): string => {
490
502
  const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
491
503
  const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
@@ -517,20 +529,62 @@ export function convertResponsesMessages(
517
529
  return `${normalizedCallId}|${normalizedItemId}`;
518
530
  };
519
531
 
520
- const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
532
+ const transformedMessages = transformMessages(transcript.messages, model, normalizeToolCallId);
533
+ const transcriptTools = resolveTranscriptTools(
534
+ transcript.messages,
535
+ (options?.supportsAdditionalTools ?? false) || (options?.supportsToolSearch ?? false),
536
+ );
521
537
  const includeSystemPrompt = options?.includeSystemPrompt ?? true;
522
- if (includeSystemPrompt && context.systemPrompt) {
523
- const supportsDeveloperRole =
524
- !isObject(model.compat) || model.compat["supportsDeveloperRole"] !== false;
525
- messages.push({
526
- role: model.reasoning && supportsDeveloperRole ? "developer" : "system",
527
- content: sanitizeSurrogates(context.systemPrompt),
528
- });
529
- }
538
+ const supportsDeveloperRole =
539
+ !isObject(model.compat) || model.compat["supportsDeveloperRole"] !== false;
540
+ const instructionRole = model.reasoning && supportsDeveloperRole ? "developer" : "system";
530
541
 
531
542
  let messageIndex = 0;
543
+ let sourceIndex = 0;
532
544
  for (const message of transformedMessages) {
533
- if (message.role === "user") {
545
+ const isLeadingSystemMessage = sourceIndex++ === 0 && message.role === "system";
546
+ if (message.role === "system") {
547
+ const addedTools =
548
+ !isLeadingSystemMessage && transcriptTools.anchorsAdditions
549
+ ? (message.toolsAdded ?? [])
550
+ : [];
551
+ const toolOptions = { ...options?.toolOptions };
552
+ if (options?.namespacedToolNames) {
553
+ toolOptions.namespacedToolNames = options.namespacedToolNames;
554
+ }
555
+ if (addedTools.length > 0 && options?.supportsAdditionalTools) {
556
+ messages.push({
557
+ type: "additional_tools",
558
+ role: "developer",
559
+ tools: convertResponsesTools(addedTools, toolOptions),
560
+ });
561
+ } else if (addedTools.length > 0 && options?.supportsToolSearch) {
562
+ const names = addedTools.map((tool) => tool.name);
563
+ const callId = `pi_tool_load_${shortHash(`system:${messageIndex}:${names.join(",")}`)}`;
564
+ messages.push({
565
+ type: "tool_search_call",
566
+ call_id: callId,
567
+ execution: "client",
568
+ status: "completed",
569
+ arguments: { query: names.join(" "), limit: names.length },
570
+ });
571
+ messages.push({
572
+ type: "tool_search_output",
573
+ call_id: callId,
574
+ execution: "client",
575
+ status: "completed",
576
+ tools: convertResponsesTools(addedTools, { ...toolOptions, deferLoading: true }),
577
+ });
578
+ }
579
+ if (isLeadingSystemMessage ? includeSystemPrompt : options?.includeSystemUpdates !== false) {
580
+ const text = isLeadingSystemMessage
581
+ ? getSystemMessageText(message)
582
+ : renderSystemMessageUpdate(message);
583
+ if (text.length > 0) {
584
+ messages.push({ role: instructionRole, content: sanitizeSurrogates(text) });
585
+ }
586
+ }
587
+ } else if (message.role === "user") {
534
588
  if (isString(message.content)) {
535
589
  messages.push({
536
590
  role: "user",
@@ -650,55 +704,8 @@ export function convertResponsesMessages(
650
704
  call_id: callId,
651
705
  output,
652
706
  });
653
-
654
- const deferredTools: Tool[] = [];
655
- for (const name of message.addedToolNames ?? []) {
656
- const tool = options?.deferredTools?.get(name);
657
- if (!tool || loadedToolNames.has(name)) continue;
658
- loadedToolNames.add(name);
659
- deferredTools.push(tool);
660
- }
661
- if (deferredTools.length > 0 && options?.deferredToolsMode === "additional-tools") {
662
- const additionalToolOptions: ConvertResponsesToolsOptions = {
663
- ...options.toolOptions,
664
- };
665
- if (options.namespacedToolNames) {
666
- additionalToolOptions.namespacedToolNames = options.namespacedToolNames;
667
- }
668
- messages.push({
669
- type: "additional_tools",
670
- role: "developer",
671
- tools: convertResponsesTools(deferredTools, additionalToolOptions),
672
- });
673
- } else if (deferredTools.length > 0 && options?.deferredToolsMode === "tool-search") {
674
- const names = deferredTools.map((tool) => tool.name);
675
- const searchCallId = `pi_tool_load_${shortHash(
676
- `${message.toolCallId}:${names.join(",")}`,
677
- )}`;
678
- messages.push({
679
- type: "tool_search_call",
680
- call_id: searchCallId,
681
- execution: "client",
682
- status: "completed",
683
- arguments: { query: names.join(" "), limit: names.length },
684
- });
685
- const deferredToolOptions: ConvertResponsesToolsOptions = {
686
- ...options?.toolOptions,
687
- deferLoading: true,
688
- };
689
- if (options?.namespacedToolNames) {
690
- deferredToolOptions.namespacedToolNames = options.namespacedToolNames;
691
- }
692
- messages.push({
693
- type: "tool_search_output",
694
- call_id: searchCallId,
695
- execution: "client",
696
- status: "completed",
697
- tools: convertResponsesTools(deferredTools, deferredToolOptions),
698
- });
699
- }
700
707
  }
701
- messageIndex++;
708
+ if (!isLeadingSystemMessage) messageIndex++;
702
709
  }
703
710
  return messages;
704
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-codex-compat",
3
- "version": "0.0.10-alpha.7",
3
+ "version": "0.0.10",
4
4
  "description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -19,7 +19,11 @@
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/2h2d-co/pi-openai-codex-compat.git"
21
21
  },
22
+ "bin": {
23
+ "pi-apply-patch": "./bin/pi-apply-patch.ts"
24
+ },
22
25
  "files": [
26
+ "bin",
23
27
  "extensions",
24
28
  "APPLY_PATCH_INSTRUCTION_FEEDBACK.md",
25
29
  "README.md",
@@ -47,9 +51,9 @@
47
51
  "devDependencies": {
48
52
  "@2h2d/oxlint-config": "0.1.1",
49
53
  "@2h2d/ts-config": "0.0.1",
50
- "@earendil-works/pi-ai": "0.85.1",
51
- "@earendil-works/pi-coding-agent": "0.85.1",
52
- "@earendil-works/pi-tui": "0.85.1",
54
+ "@earendil-works/pi-ai": "0.86.0",
55
+ "@earendil-works/pi-coding-agent": "0.86.0",
56
+ "@earendil-works/pi-tui": "0.86.0",
53
57
  "@types/node": "22.20.1",
54
58
  "oxfmt": "0.64.0",
55
59
  "oxlint": "1.79.0",
@@ -58,9 +62,9 @@
58
62
  "typescript": "7.0.2"
59
63
  },
60
64
  "peerDependencies": {
61
- "@earendil-works/pi-ai": ">=0.84.0 <0.86.0",
62
- "@earendil-works/pi-coding-agent": ">=0.84.0 <0.86.0",
63
- "@earendil-works/pi-tui": ">=0.84.0 <0.86.0",
65
+ "@earendil-works/pi-ai": ">=0.86.0 <0.87.0",
66
+ "@earendil-works/pi-coding-agent": ">=0.86.0 <0.87.0",
67
+ "@earendil-works/pi-tui": ">=0.86.0 <0.87.0",
64
68
  "typebox": "*"
65
69
  },
66
70
  "engines": {