@narumitw/pi-codex-compact 0.52.0 → 0.53.0

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.
@@ -1,306 +1,302 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import type { Api, Context, Model, Tool } from "@earendil-works/pi-ai";
3
3
  import {
4
- buildContextEntries,
5
- buildSessionContext,
6
- convertToLlm,
7
- type ExtensionAPI,
8
- type ExtensionContext,
9
- type SessionBeforeCompactEvent,
10
- sessionEntryToContextMessages,
4
+ buildContextEntries,
5
+ buildSessionContext,
6
+ convertToLlm,
7
+ type ExtensionAPI,
8
+ type ExtensionContext,
9
+ type SessionBeforeCompactEvent,
10
+ sessionEntryToContextMessages,
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import {
13
- buildReplacementHistory,
14
- type CodexCheckpointDetails,
15
- checkpointMarker,
16
- createCheckpointDetails,
17
- fallbackSummary,
18
- latestCheckpoint,
19
- projectCheckpointContext,
13
+ buildReplacementHistory,
14
+ type CodexCheckpointDetails,
15
+ checkpointMarker,
16
+ createCheckpointDetails,
17
+ fallbackSummary,
18
+ latestCheckpoint,
19
+ projectCheckpointContext,
20
20
  } from "./checkpoint.js";
21
- import { resolveCompactionRoute, usesResponsesCompactionApi } from "./model-api.js";
21
+ import { type CompactionRoute, resolveCompactionRoute } from "./model-api.js";
22
22
  import { hasCheckpointMarker, rewriteCheckpointMarker } from "./protocol.js";
23
23
  import { requestRemoteCompaction } from "./remote.js";
24
24
  import {
25
- type CodexCompactSettings,
26
- type CodexCompactSettingsRuntime,
27
- type CodexCompactSettingsState,
28
- createCodexCompactSettingsRuntime,
25
+ type CodexCompactSettings,
26
+ type CodexCompactSettingsRuntime,
27
+ type CodexCompactSettingsState,
28
+ createCodexCompactSettingsRuntime,
29
29
  } from "./settings.js";
30
30
  import { terminalText } from "./terminal.js";
31
31
 
32
32
  const STATUS_KEY = "codex-compact";
33
33
 
34
34
  function activeCheckpoint(ctx: ExtensionContext) {
35
- return latestCheckpoint(ctx.sessionManager.getBranch());
35
+ return latestCheckpoint(ctx.sessionManager.getBranch());
36
36
  }
37
37
 
38
38
  function isCheckpointCompatible(
39
- details: CodexCheckpointDetails,
40
- model: Model<Api> | undefined,
39
+ details: CodexCheckpointDetails,
40
+ model: Model<Api> | undefined,
41
+ settings: CodexCompactSettings,
41
42
  ): boolean {
42
- return (
43
- usesResponsesCompactionApi(model) && model.api === details.api && model.id === details.modelId
44
- );
43
+ const route = resolveCompactionRoute(model, settings);
44
+ return (
45
+ route.kind === "remote" &&
46
+ model !== undefined &&
47
+ route.api === details.api &&
48
+ route.profile === details.profile &&
49
+ model.id === details.modelId
50
+ );
45
51
  }
46
52
 
47
53
  function keptMessages(event: SessionBeforeCompactEvent): AgentMessage[] {
48
- const leafId = event.branchEntries.at(-1)?.id ?? null;
49
- const contextEntries = buildContextEntries(event.branchEntries, leafId);
50
- const keptIndex = contextEntries.findIndex(
51
- (entry) => entry.id === event.preparation.firstKeptEntryId,
52
- );
53
- if (keptIndex < 0) {
54
- throw new Error("Pi compaction cut point is not present in the active context");
55
- }
56
- return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
54
+ const leafId = event.branchEntries.at(-1)?.id ?? null;
55
+ const contextEntries = buildContextEntries(event.branchEntries, leafId);
56
+ const keptIndex = contextEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId);
57
+ if (keptIndex < 0) {
58
+ throw new Error("Pi compaction cut point is not present in the active context");
59
+ }
60
+ return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
57
61
  }
58
62
 
59
63
  function activeTools(pi: ExtensionAPI): Tool[] {
60
- const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
61
- return pi.getActiveTools().flatMap((name) => {
62
- const tool = available.get(name);
63
- return tool
64
- ? [
65
- {
66
- name: tool.name,
67
- description: tool.description,
68
- parameters: tool.parameters,
69
- },
70
- ]
71
- : [];
72
- });
64
+ const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
65
+ return pi.getActiveTools().flatMap((name) => {
66
+ const tool = available.get(name);
67
+ return tool
68
+ ? [
69
+ {
70
+ name: tool.name,
71
+ description: tool.description,
72
+ parameters: tool.parameters,
73
+ },
74
+ ]
75
+ : [];
76
+ });
73
77
  }
74
78
 
75
79
  function projectedCurrentMessages(
76
- event: SessionBeforeCompactEvent,
77
- model: Model<Api>,
80
+ event: SessionBeforeCompactEvent,
81
+ model: Model<Api>,
82
+ route: Extract<CompactionRoute, { kind: "remote" }>,
78
83
  ): { messages: AgentMessage[]; prior?: CodexCheckpointDetails } {
79
- const leafId = event.branchEntries.at(-1)?.id ?? null;
80
- const session = buildSessionContext(event.branchEntries, leafId);
81
- const prior = latestCheckpoint(event.branchEntries);
82
- if (!prior) return { messages: session.messages };
83
- if (prior.details.api !== model.api || prior.details.modelId !== model.id) {
84
- throw new Error("The active opaque checkpoint belongs to a different Responses model");
85
- }
86
- const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
87
- if (!projected) {
88
- throw new Error("The previous opaque checkpoint could not be projected safely");
89
- }
90
- return { messages: projected, prior: prior.details };
84
+ const leafId = event.branchEntries.at(-1)?.id ?? null;
85
+ const session = buildSessionContext(event.branchEntries, leafId);
86
+ const prior = latestCheckpoint(event.branchEntries);
87
+ if (!prior) return { messages: session.messages };
88
+ if (
89
+ prior.details.api !== route.api ||
90
+ prior.details.profile !== route.profile ||
91
+ prior.details.modelId !== model.id
92
+ ) {
93
+ throw new Error("The active opaque checkpoint belongs to a different Responses model");
94
+ }
95
+ const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
96
+ if (!projected) {
97
+ throw new Error("The previous opaque checkpoint could not be projected safely");
98
+ }
99
+ return { messages: projected, prior: prior.details };
91
100
  }
92
101
 
93
- function notifyFailure(
94
- ctx: ExtensionContext,
95
- error: unknown,
96
- settings: CodexCompactSettings,
97
- ): void {
98
- if (!ctx.hasUI || !settings.notifyOnFallback) return;
99
- const message = terminalText(error instanceof Error ? error.message : String(error));
100
- ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
102
+ function notifyFailure(ctx: ExtensionContext, error: unknown, settings: CodexCompactSettings): void {
103
+ if (!ctx.hasUI || !settings.notifyOnFallback) return;
104
+ const message = terminalText(error instanceof Error ? error.message : String(error));
105
+ ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
101
106
  }
102
107
 
103
108
  function sessionStillOwned(ctx: ExtensionContext, sessionId: string, signal: AbortSignal): boolean {
104
- return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
109
+ return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
105
110
  }
106
111
 
107
112
  async function compactRemotely(
108
- pi: ExtensionAPI,
109
- event: SessionBeforeCompactEvent,
110
- ctx: ExtensionContext,
111
- settings: CodexCompactSettings,
112
- ownerSignal: AbortSignal,
113
- fetch?: typeof globalThis.fetch,
113
+ pi: ExtensionAPI,
114
+ event: SessionBeforeCompactEvent,
115
+ ctx: ExtensionContext,
116
+ settings: CodexCompactSettings,
117
+ ownerSignal: AbortSignal,
118
+ fetch?: typeof globalThis.fetch,
114
119
  ) {
115
- const model = ctx.model;
116
- const route = resolveCompactionRoute(model, settings);
117
- if (route.kind === "native" || !usesResponsesCompactionApi(model)) return undefined;
118
- const signal = AbortSignal.any([event.signal, ownerSignal]);
119
- if (signal.aborted) return { cancel: true };
120
- const sessionId = ctx.sessionManager.getSessionId();
121
- ctx.ui.setStatus(
122
- STATUS_KEY,
123
- route.protocol === "remote-v2" ? "Responses Remote V2…" : "Responses Compact API…",
124
- );
125
- try {
126
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
127
- if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
128
- if (!auth.ok) throw new Error(auth.error);
129
- const provider = ctx.modelRegistry.getProvider(model.provider);
130
- if (!provider) throw new Error("The active Responses provider is unavailable");
131
- const current = projectedCurrentMessages(event, model);
132
- const context: Context = {
133
- systemPrompt: ctx.getSystemPrompt(),
134
- messages: convertToLlm(current.messages),
135
- tools: activeTools(pi),
136
- };
137
- const response = await requestRemoteCompaction({
138
- provider,
139
- model,
140
- context,
141
- protocol: route.protocol,
142
- apiKey: auth.apiKey,
143
- headers: auth.headers,
144
- env: auth.env,
145
- signal,
146
- priorCheckpoint: current.prior
147
- ? {
148
- marker: checkpointMarker(current.prior.checkpointId),
149
- replacementHistory: current.prior.replacementHistory,
150
- }
151
- : undefined,
152
- requestTimeoutMs: settings.requestTimeoutMs,
153
- maxRetries: settings.maxRetries,
154
- fetch,
155
- });
156
- if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
157
- const replacementHistory = buildReplacementHistory(
158
- response.compactedOutput?.slice(0, -1) ?? response.promptInput,
159
- response.item,
160
- { tokenBudget: settings.replacementTokenBudget },
161
- );
162
- const details = createCheckpointDetails({
163
- provider: model.provider,
164
- api: route.api,
165
- modelId: model.id,
166
- protocol: route.protocol,
167
- replacementHistory,
168
- keptMessages: keptMessages(event),
169
- });
170
- return {
171
- compaction: {
172
- summary: fallbackSummary(details.checkpointId),
173
- firstKeptEntryId: event.preparation.firstKeptEntryId,
174
- tokensBefore: event.preparation.tokensBefore,
175
- usage: response.usage,
176
- details,
177
- },
178
- };
179
- } catch (error) {
180
- if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
181
- return { cancel: true };
182
- }
183
- notifyFailure(ctx, error, settings);
184
- return undefined;
185
- } finally {
186
- if (ctx.sessionManager.getSessionId() === sessionId) ctx.ui.setStatus(STATUS_KEY, undefined);
187
- }
120
+ const model = ctx.model;
121
+ const route = resolveCompactionRoute(model, settings);
122
+ if (route.kind === "native" || !model) return undefined;
123
+ const signal = AbortSignal.any([event.signal, ownerSignal]);
124
+ if (signal.aborted) return { cancel: true };
125
+ const sessionId = ctx.sessionManager.getSessionId();
126
+ ctx.ui.setStatus(STATUS_KEY, route.protocol === "remote-v2" ? "Responses Remote V2…" : "Responses Compact API…");
127
+ try {
128
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
129
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
130
+ if (!auth.ok) throw new Error(auth.error);
131
+ const provider = ctx.modelRegistry.getProvider(model.provider);
132
+ if (!provider) throw new Error("The active Responses provider is unavailable");
133
+ const current = projectedCurrentMessages(event, model, route);
134
+ const context: Context = {
135
+ systemPrompt: ctx.getSystemPrompt(),
136
+ messages: convertToLlm(current.messages),
137
+ tools: activeTools(pi),
138
+ };
139
+ const response = await requestRemoteCompaction({
140
+ provider,
141
+ model,
142
+ context,
143
+ protocol: route.protocol,
144
+ profile: route.profile,
145
+ apiKey: auth.apiKey,
146
+ headers: auth.headers,
147
+ env: auth.env,
148
+ signal,
149
+ priorCheckpoint: current.prior
150
+ ? {
151
+ marker: checkpointMarker(current.prior.checkpointId),
152
+ replacementHistory: current.prior.replacementHistory,
153
+ }
154
+ : undefined,
155
+ requestTimeoutMs: settings.requestTimeoutMs,
156
+ maxRetries: settings.maxRetries,
157
+ fetch,
158
+ });
159
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
160
+ const replacementHistory = buildReplacementHistory(
161
+ response.compactedOutput?.slice(0, -1) ?? response.promptInput,
162
+ response.item,
163
+ { tokenBudget: settings.replacementTokenBudget },
164
+ );
165
+ const details = createCheckpointDetails({
166
+ provider: model.provider,
167
+ api: route.api,
168
+ profile: route.profile,
169
+ modelId: model.id,
170
+ protocol: route.protocol,
171
+ replacementHistory,
172
+ keptMessages: keptMessages(event),
173
+ });
174
+ return {
175
+ compaction: {
176
+ summary: fallbackSummary(details.checkpointId),
177
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
178
+ tokensBefore: event.preparation.tokensBefore,
179
+ usage: response.usage,
180
+ details,
181
+ },
182
+ };
183
+ } catch (error) {
184
+ if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
185
+ return { cancel: true };
186
+ }
187
+ notifyFailure(ctx, error, settings);
188
+ return undefined;
189
+ } finally {
190
+ if (ctx.sessionManager.getSessionId() === sessionId) ctx.ui.setStatus(STATUS_KEY, undefined);
191
+ }
188
192
  }
189
193
 
190
194
  export function createCodexCompactExtension(
191
- options: { fetch?: typeof globalThis.fetch; settingsRuntime?: CodexCompactSettingsRuntime } = {},
195
+ options: { fetch?: typeof globalThis.fetch; settingsRuntime?: CodexCompactSettingsRuntime } = {},
192
196
  ): (pi: ExtensionAPI) => void {
193
- return (pi) => {
194
- const providerWarnings = new Set<string>();
195
- const settingsRuntime = options.settingsRuntime ?? createCodexCompactSettingsRuntime();
196
- let sessionController = new AbortController();
197
- let generation = 0;
197
+ return (pi) => {
198
+ const providerWarnings = new Set<string>();
199
+ const settingsRuntime = options.settingsRuntime ?? createCodexCompactSettingsRuntime();
200
+ let sessionController = new AbortController();
201
+ let generation = 0;
198
202
 
199
- pi.registerCommand("codex-compact", {
200
- description: "Compact now or configure Responses compaction",
201
- handler: async (args, ctx) => {
202
- if (args.trim()) throw new Error("Usage: /codex-compact");
203
- const ownerGeneration = generation;
204
- const controller = sessionController;
205
- const { showCodexCompactMenu } = await import("./settings-menu.js");
206
- if (ownerGeneration !== generation || controller.signal.aborted) return;
207
- await showCodexCompactMenu(settingsRuntime, ctx, {
208
- signal: controller.signal,
209
- isCurrent: () => ownerGeneration === generation && !controller.signal.aborted,
210
- });
211
- },
212
- });
203
+ pi.registerCommand("codex-compact", {
204
+ description: "Compact now or configure Responses compaction",
205
+ handler: async (args, ctx) => {
206
+ if (args.trim()) throw new Error("Usage: /codex-compact");
207
+ const ownerGeneration = generation;
208
+ const controller = sessionController;
209
+ const { showCodexCompactMenu } = await import("./settings-menu.js");
210
+ if (ownerGeneration !== generation || controller.signal.aborted) return;
211
+ await showCodexCompactMenu(settingsRuntime, ctx, {
212
+ signal: controller.signal,
213
+ isCurrent: () => ownerGeneration === generation && !controller.signal.aborted,
214
+ });
215
+ },
216
+ });
213
217
 
214
- pi.on("session_start", async (_event, ctx) => {
215
- sessionController.abort();
216
- sessionController = new AbortController();
217
- generation += 1;
218
- const ownerGeneration = generation;
219
- const sessionId = ctx.sessionManager.getSessionId();
220
- providerWarnings.clear();
221
- let state: Readonly<CodexCompactSettingsState>;
222
- try {
223
- state = await settingsRuntime.reload(sessionController.signal);
224
- } catch (error) {
225
- if (sessionController.signal.aborted || ownerGeneration !== generation) return;
226
- if (ctx.hasUI) {
227
- ctx.ui.notify(
228
- `Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
229
- "warning",
230
- );
231
- }
232
- return;
233
- }
234
- if (
235
- sessionController.signal.aborted ||
236
- ownerGeneration !== generation ||
237
- ctx.sessionManager.getSessionId() !== sessionId
238
- ) {
239
- return;
240
- }
241
- if (ctx.hasUI && state.kind === "invalid") {
242
- ctx.ui.notify(
243
- `Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
244
- "warning",
245
- );
246
- }
247
- });
218
+ pi.on("session_start", async (_event, ctx) => {
219
+ sessionController.abort();
220
+ sessionController = new AbortController();
221
+ generation += 1;
222
+ const ownerGeneration = generation;
223
+ const sessionId = ctx.sessionManager.getSessionId();
224
+ providerWarnings.clear();
225
+ let state: Readonly<CodexCompactSettingsState>;
226
+ try {
227
+ state = await settingsRuntime.reload(sessionController.signal);
228
+ } catch (error) {
229
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return;
230
+ if (ctx.hasUI) {
231
+ ctx.ui.notify(
232
+ `Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
233
+ "warning",
234
+ );
235
+ }
236
+ return;
237
+ }
238
+ if (
239
+ sessionController.signal.aborted ||
240
+ ownerGeneration !== generation ||
241
+ ctx.sessionManager.getSessionId() !== sessionId
242
+ ) {
243
+ return;
244
+ }
245
+ if (ctx.hasUI && state.kind === "invalid") {
246
+ ctx.ui.notify(
247
+ `Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
248
+ "warning",
249
+ );
250
+ }
251
+ });
248
252
 
249
- pi.on("session_before_compact", (event, ctx) =>
250
- compactRemotely(
251
- pi,
252
- event,
253
- ctx,
254
- settingsRuntime.get().settings,
255
- sessionController.signal,
256
- options.fetch,
257
- ),
258
- );
253
+ pi.on("session_before_compact", (event, ctx) =>
254
+ compactRemotely(pi, event, ctx, settingsRuntime.get().settings, sessionController.signal, options.fetch),
255
+ );
259
256
 
260
- pi.on("context", (event, ctx) => {
261
- if (!settingsRuntime.get().settings.enabled) return undefined;
262
- const checkpoint = activeCheckpoint(ctx);
263
- if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model)) return undefined;
264
- const messages = projectCheckpointContext(
265
- event.messages,
266
- checkpoint.details,
267
- checkpoint.entry.summary,
268
- );
269
- return messages ? { messages } : undefined;
270
- });
257
+ pi.on("context", (event, ctx) => {
258
+ if (!settingsRuntime.get().settings.enabled) return undefined;
259
+ const settings = settingsRuntime.get().settings;
260
+ const checkpoint = activeCheckpoint(ctx);
261
+ if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
262
+ const messages = projectCheckpointContext(event.messages, checkpoint.details, checkpoint.entry.summary);
263
+ return messages ? { messages } : undefined;
264
+ });
271
265
 
272
- pi.on("before_provider_request", (event, ctx) => {
273
- if (!settingsRuntime.get().settings.enabled) return undefined;
274
- const checkpoint = activeCheckpoint(ctx);
275
- if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model)) return undefined;
276
- const marker = checkpointMarker(checkpoint.details.checkpointId);
277
- if (!hasCheckpointMarker(event.payload, marker)) return undefined;
278
- return rewriteCheckpointMarker(event.payload, marker, checkpoint.details.replacementHistory);
279
- });
266
+ pi.on("before_provider_request", (event, ctx) => {
267
+ if (!settingsRuntime.get().settings.enabled) return undefined;
268
+ const settings = settingsRuntime.get().settings;
269
+ const checkpoint = activeCheckpoint(ctx);
270
+ if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
271
+ const marker = checkpointMarker(checkpoint.details.checkpointId);
272
+ if (!hasCheckpointMarker(event.payload, marker)) return undefined;
273
+ return rewriteCheckpointMarker(event.payload, marker, checkpoint.details.replacementHistory);
274
+ });
280
275
 
281
- pi.on("model_select", (event, ctx) => {
282
- if (!settingsRuntime.get().settings.enabled) return;
283
- const checkpoint = activeCheckpoint(ctx);
284
- if (!checkpoint || isCheckpointCompatible(checkpoint.details, event.model)) return;
285
- const key = `${ctx.sessionManager.getSessionId()}:${event.model.provider}:${event.model.id}`;
286
- if (providerWarnings.has(key)) return;
287
- providerWarnings.add(key);
288
- if (ctx.hasUI) {
289
- ctx.ui.notify(
290
- "The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
291
- "warning",
292
- );
293
- }
294
- });
276
+ pi.on("model_select", (event, ctx) => {
277
+ if (!settingsRuntime.get().settings.enabled) return;
278
+ const settings = settingsRuntime.get().settings;
279
+ const checkpoint = activeCheckpoint(ctx);
280
+ if (!checkpoint || isCheckpointCompatible(checkpoint.details, event.model, settings)) return;
281
+ const key = `${ctx.sessionManager.getSessionId()}:${event.model.provider}:${event.model.id}`;
282
+ if (providerWarnings.has(key)) return;
283
+ providerWarnings.add(key);
284
+ if (ctx.hasUI) {
285
+ ctx.ui.notify(
286
+ "The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
287
+ "warning",
288
+ );
289
+ }
290
+ });
295
291
 
296
- pi.on("session_shutdown", async (_event, ctx) => {
297
- generation += 1;
298
- sessionController.abort();
299
- providerWarnings.clear();
300
- ctx.ui.setStatus(STATUS_KEY, undefined);
301
- await settingsRuntime.flush();
302
- });
303
- };
292
+ pi.on("session_shutdown", async (_event, ctx) => {
293
+ generation += 1;
294
+ sessionController.abort();
295
+ providerWarnings.clear();
296
+ ctx.ui.setStatus(STATUS_KEY, undefined);
297
+ await settingsRuntime.flush();
298
+ });
299
+ };
304
300
  }
305
301
 
306
302
  export default createCodexCompactExtension();
package/src/model-api.ts CHANGED
@@ -1,48 +1,59 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import { hasApi } from "@earendil-works/pi-ai";
2
+ import type { CodexCompactSettings } from "./settings.js";
3
3
 
4
4
  export const RESPONSES_COMPACTION_APIS = [
5
- "openai-codex-responses",
6
- "openai-responses",
7
- "azure-openai-responses",
5
+ "openai-codex-responses",
6
+ "openai-responses",
7
+ "azure-openai-responses",
8
8
  ] as const;
9
9
 
10
- export type ResponsesCompactionApi = (typeof RESPONSES_COMPACTION_APIS)[number];
10
+ export type BuiltInResponsesCompactionApi = (typeof RESPONSES_COMPACTION_APIS)[number];
11
+ export type ResponsesCompactionApi = BuiltInResponsesCompactionApi;
12
+ export type ResponsesCompactionProfile = "codex-responses-v1" | "openai-responses-v1";
11
13
  export type RemoteCompactionProtocol = "remote-v2" | "responses-compact";
12
14
  export type RemoteCompactionProtocolSetting = "auto" | RemoteCompactionProtocol;
13
15
 
14
16
  export type CompactionRoute =
15
- | { kind: "remote"; protocol: RemoteCompactionProtocol; api: ResponsesCompactionApi }
16
- | { kind: "native"; reason: string };
17
+ | { kind: "remote"; protocol: RemoteCompactionProtocol; api: Api; profile: ResponsesCompactionProfile }
18
+ | { kind: "native"; reason: string };
17
19
 
18
- export function usesResponsesCompactionApi(
19
- model: Model<Api> | undefined,
20
- ): model is Model<ResponsesCompactionApi> {
21
- return model !== undefined && RESPONSES_COMPACTION_APIS.some((api) => hasApi(model, api));
20
+ function resolveResponsesCompactionProfile(
21
+ api: Api | undefined,
22
+ apiProfiles: Readonly<Record<string, "codex-responses-v1">> = {},
23
+ ): ResponsesCompactionProfile | undefined {
24
+ if (api === "openai-codex-responses") return "codex-responses-v1";
25
+ if (api === "openai-responses" || api === "azure-openai-responses") return "openai-responses-v1";
26
+ return api && Object.hasOwn(apiProfiles, api) && apiProfiles[api] === "codex-responses-v1"
27
+ ? "codex-responses-v1"
28
+ : undefined;
22
29
  }
23
30
 
24
31
  export function resolveCompactionRouteForApi(
25
- api: Api | undefined,
26
- options: { enabled: boolean; protocol: RemoteCompactionProtocolSetting },
32
+ api: Api | undefined,
33
+ options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
34
+ apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
35
+ },
27
36
  ): CompactionRoute {
28
- if (!options.enabled) return { kind: "native", reason: "remote compaction is disabled" };
29
- if (!api) return { kind: "native", reason: "no active model" };
30
- if (!RESPONSES_COMPACTION_APIS.includes(api as ResponsesCompactionApi)) {
31
- return { kind: "native", reason: `API ${api} does not support Responses compaction` };
32
- }
33
- const supportedApi = api as ResponsesCompactionApi;
34
- const protocol =
35
- options.protocol === "auto"
36
- ? supportedApi === "openai-codex-responses"
37
- ? "remote-v2"
38
- : "responses-compact"
39
- : options.protocol;
40
- return { kind: "remote", protocol, api: supportedApi };
37
+ if (!options.enabled) return { kind: "native", reason: "remote compaction is disabled" };
38
+ if (!api) return { kind: "native", reason: "no active model" };
39
+ const profile = resolveResponsesCompactionProfile(api, options.apiProfiles);
40
+ if (!profile) {
41
+ return { kind: "native", reason: `API ${api} does not support Responses compaction` };
42
+ }
43
+ const protocol =
44
+ options.protocol === "auto"
45
+ ? profile === "codex-responses-v1"
46
+ ? "remote-v2"
47
+ : "responses-compact"
48
+ : options.protocol;
49
+ return { kind: "remote", protocol, api, profile };
41
50
  }
42
51
 
43
52
  export function resolveCompactionRoute(
44
- model: Model<Api> | undefined,
45
- options: { enabled: boolean; protocol: RemoteCompactionProtocolSetting },
53
+ model: Model<Api> | undefined,
54
+ options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
55
+ apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
56
+ },
46
57
  ): CompactionRoute {
47
- return resolveCompactionRouteForApi(model?.api, options);
58
+ return resolveCompactionRouteForApi(model?.api, options);
48
59
  }