pi-openai-codex-compat 0.0.10-alpha.8 → 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,13 @@
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.
7
12
  - Support `gpt-6-astra` for opt-in Responses Lite requests and `pro` reasoning
8
13
  mode, without changing either setting's default.
9
14
 
@@ -21,6 +26,11 @@
21
26
 
22
27
  ### Fixed
23
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.
24
34
  - Show effective unified-exec yield durations beside accent-colored command
25
35
  tool titles, include explicit working directories, emphasize commands on
26
36
  their own separated rows, separate `apply_patch` results, and mute command
@@ -83,7 +93,7 @@
83
93
 
84
94
  ### Changed
85
95
 
86
- - 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.
87
97
  - Describe `shell_command`, `exec_command`, and `write_stdin` in Pi's tool list,
88
98
  guidelines, and detailed tool contracts using the actual resolved default
89
99
  shell. Document command working-directory and `PI_*` context behavior,
@@ -109,9 +119,10 @@
109
119
  stderr while retaining Pi's complete-output files.
110
120
  - Replace the multi-line startup configuration dump with a concise package and
111
121
  settings-command notice, and simplify the fallback settings-save error.
112
- - Align deferred tool loading with Pi 0.84.3, preferring message-anchored
113
- additional tools on capable Codex models while retaining tool-search and
114
- 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.
115
126
  - Simplify `apply_patch` for its queued single-writer operating model: retain
116
127
  Pi's mutation queue, remove the extension-local alias queue and
117
128
  preflight-to-execution drift machinery, use direct writes for in-place text
package/README.md CHANGED
@@ -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:
@@ -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. |
@@ -478,6 +479,23 @@ cannot be inspected, it says that the final state was not verified.
478
479
  The complete feedback and rendering contract is documented in
479
480
  [`APPLY_PATCH_INSTRUCTION_FEEDBACK.md`](APPLY_PATCH_INSTRUCTION_FEEDBACK.md).
480
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
+
481
499
  ## `image_gen.imagegen`
482
500
 
483
501
  The package registers the dotted Pi tool name `image_gen.imagegen` and serializes it as a native Responses API namespace:
@@ -548,8 +566,7 @@ Both namespace tools are accepted only from the fixed extension-owned allowlist.
548
566
  mise trust
549
567
  mise install
550
568
  npm install
551
- npm run check
552
- npm test
569
+ mise run check
553
570
  npm run pack:dry
554
571
  ```
555
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,
@@ -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.8",
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": {