@criogaid/pi-codex-compaction 0.4.1 → 0.5.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.
- package/CHANGELOG.md +18 -0
- package/README.md +23 -30
- package/package.json +7 -5
- package/src/fallback-settings.ts +59 -25
- package/src/index.ts +43 -38
- package/src/protocol.ts +29 -1
- package/src/remote-retry.ts +277 -0
- package/src/remote.ts +142 -69
- package/src/request-snapshot.ts +109 -36
package/src/request-snapshot.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// Own ordinary-request prompt and context snapshots that let compaction reuse Pi's projected request prefix,
|
|
2
2
|
// and build compaction's provider context the way Pi 0.99 builds an ordinary request.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
5
|
import { getCurrentSystemMessage, getSystemMessageText, type Context, type Message, type Tool } from "@earendil-works/pi-ai";
|
|
5
6
|
import { convertToLlm, type ExtensionAPI, type ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { sameBackend, sameModel, type CapableModel, type ProviderIdentity } from "./capability.js";
|
|
7
8
|
import { type CodexCheckpointDetails, fingerprintMessage, projectCheckpointRequest, withoutSystemMessages } from "./checkpoint.js";
|
|
8
|
-
import { isObject, type JsonObject } from "./protocol.js";
|
|
9
|
+
import { BLOCKED_IMAGE_TEXT, CodexCompactionProtocolError, isObject, type JsonObject } from "./protocol.js";
|
|
10
|
+
import { contextUserItems, userItemOrigins } from "./retention-input.js";
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* In Pi 0.99.1 and 1.0.4 these settings affect presentation, not provider inputs. Every other setting,
|
|
14
|
+
* including unknown future ones, stays in the reuse key, so changing it drops the wire snapshot.
|
|
15
|
+
*/
|
|
16
|
+
const INTERFACE_ONLY_SETTINGS: ReadonlySet<string> = new Set([
|
|
17
|
+
"autocompleteMaxVisible", "collapseChangelog", "doubleEscapeAction", "editorPaddingX", "externalEditor",
|
|
18
|
+
"fullscreenCopyOnSelect", "fullscreenExitOutput", "fullscreenScrollbar", "fullscreenWheelScrollLines",
|
|
19
|
+
"hideThinkingBlock", "lastChangelogVersion", "markdown", "outputPad", "quietStartup", "showCacheMissNotices",
|
|
20
|
+
"showHardwareCursor", "terminal", "theme", "treeFilterMode", "tuiMode", "warnings",
|
|
21
|
+
]);
|
|
11
22
|
|
|
12
23
|
interface SnapshotScope {
|
|
13
24
|
readonly sessionId: string;
|
|
@@ -73,10 +84,12 @@ export function promptOverrideFor(
|
|
|
73
84
|
sessionId: string,
|
|
74
85
|
target: CapableModel,
|
|
75
86
|
messages: readonly AgentMessage[],
|
|
87
|
+
currentPrompt: string,
|
|
76
88
|
): PromptOverride | undefined {
|
|
77
89
|
const scoped = snapshotFor(override, sessionId, target);
|
|
78
90
|
const head = scoped && sourceHead(messages);
|
|
79
|
-
return head && fingerprintMessage(head) === scoped.sourceFingerprint
|
|
91
|
+
return head && fingerprintMessage(head) === scoped.sourceFingerprint &&
|
|
92
|
+
(currentPrompt === scoped.text || currentPrompt === getSystemMessageText(head)) ? scoped : undefined;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
export function captureContextSnapshot(
|
|
@@ -127,6 +140,9 @@ interface ProviderRequestFields {
|
|
|
127
140
|
readonly instructions?: string;
|
|
128
141
|
readonly tools?: readonly JsonObject[];
|
|
129
142
|
readonly reasoning?: JsonObject;
|
|
143
|
+
readonly parallel_tool_calls?: boolean;
|
|
144
|
+
readonly text?: JsonObject;
|
|
145
|
+
readonly prompt_cache_options?: { readonly mode?: string; readonly ttl?: string };
|
|
130
146
|
readonly prompt_cache_key?: string;
|
|
131
147
|
readonly prompt_cache_retention?: string;
|
|
132
148
|
readonly service_tier?: string;
|
|
@@ -134,20 +150,24 @@ interface ProviderRequestFields {
|
|
|
134
150
|
|
|
135
151
|
export interface ProviderRequestSnapshot extends SnapshotScope {
|
|
136
152
|
readonly sourceFingerprints: readonly string[];
|
|
153
|
+
/** The effective prompt `ctx.getSystemPrompt()` reported for the observed request. */
|
|
154
|
+
readonly systemPrompt: string;
|
|
137
155
|
readonly inputsKey: string;
|
|
156
|
+
/** The same inputs after the observed run settled and Pi fell back to its base prompt options. */
|
|
157
|
+
readonly settledInputsKey?: string;
|
|
138
158
|
readonly fields: ProviderRequestFields;
|
|
139
|
-
|
|
140
|
-
readonly prefix
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return end < 0 ? input : input.slice(0, end);
|
|
159
|
+
/** Absent when the observed input depends on server-side conversation state. */
|
|
160
|
+
readonly prefix?: {
|
|
161
|
+
readonly boundary: string;
|
|
162
|
+
readonly contextLength: number;
|
|
163
|
+
readonly input: readonly JsonObject[];
|
|
164
|
+
readonly contextual: readonly boolean[];
|
|
165
|
+
};
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
function requestInputsKey(target: CapableModel, inputs: ProviderRequestInputs): string {
|
|
150
|
-
|
|
169
|
+
const settings = Object.fromEntries(Object.entries(inputs.settings).filter(([key]) => !INTERFACE_ONLY_SETTINGS.has(key)));
|
|
170
|
+
return JSON.stringify({ model: target.model, ...inputs, settings });
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
function captureProviderRequest(
|
|
@@ -155,47 +175,77 @@ function captureProviderRequest(
|
|
|
155
175
|
): ProviderRequestSnapshot | undefined {
|
|
156
176
|
if (!isObject(payload) || payload.model !== target.model.id ||
|
|
157
177
|
!Array.isArray(payload.input) || !payload.input.every(isObject)) return undefined;
|
|
158
|
-
const { instructions, tools, reasoning, prompt_cache_key, prompt_cache_retention, service_tier, text } = payload;
|
|
178
|
+
const { instructions, tools, reasoning, parallel_tool_calls, prompt_cache_key, prompt_cache_retention, prompt_cache_options, service_tier, text } = payload;
|
|
159
179
|
const verbosity = isObject(text) ? text.verbosity : undefined;
|
|
180
|
+
const format = isObject(text) ? text.format : undefined;
|
|
181
|
+
const mode = isObject(prompt_cache_options) ? prompt_cache_options.mode : undefined;
|
|
182
|
+
const ttl = isObject(prompt_cache_options) ? prompt_cache_options.ttl : undefined;
|
|
160
183
|
if ((instructions !== undefined && typeof instructions !== "string") ||
|
|
161
184
|
(tools !== undefined && (!Array.isArray(tools) || !tools.every(isObject))) ||
|
|
162
185
|
(reasoning !== undefined && !isObject(reasoning)) ||
|
|
186
|
+
(parallel_tool_calls !== undefined && typeof parallel_tool_calls !== "boolean") ||
|
|
187
|
+
(format !== undefined && !isObject(format)) ||
|
|
188
|
+
(prompt_cache_options !== undefined && !isObject(prompt_cache_options)) ||
|
|
189
|
+
(mode !== undefined && typeof mode !== "string") || (ttl !== undefined && typeof ttl !== "string") ||
|
|
163
190
|
(prompt_cache_key !== undefined && typeof prompt_cache_key !== "string") ||
|
|
164
191
|
(prompt_cache_retention !== undefined && typeof prompt_cache_retention !== "string") ||
|
|
165
192
|
(service_tier !== undefined && typeof service_tier !== "string") ||
|
|
166
193
|
(text !== undefined && !isObject(text)) || (verbosity !== undefined && typeof verbosity !== "string")) return undefined;
|
|
194
|
+
const contextual = contextUserItems(payload.input, userItemOrigins(context.messages));
|
|
195
|
+
// Copy policy only: a diagnostic response ID or prewarm operation belongs to the observed request.
|
|
196
|
+
const cachePolicy = mode !== undefined || ttl !== undefined ? { mode, ttl } : undefined;
|
|
197
|
+
const serverHistory = payload.previous_response_id != null || payload.conversation != null ||
|
|
198
|
+
payload.input.some((item) => item.type === "item_reference");
|
|
167
199
|
return {
|
|
168
200
|
sessionId: context.sessionId, identity: context.identity, sourceFingerprints: context.sourceFingerprints,
|
|
169
|
-
inputsKey: requestInputsKey(target, inputs),
|
|
170
|
-
fields: structuredClone({ instructions, tools, reasoning,
|
|
171
|
-
|
|
201
|
+
systemPrompt: inputs.systemPrompt, inputsKey: requestInputsKey(target, inputs),
|
|
202
|
+
fields: structuredClone({ instructions, tools, reasoning, parallel_tool_calls, text, prompt_cache_key,
|
|
203
|
+
prompt_cache_retention, prompt_cache_options: cachePolicy, service_tier }),
|
|
204
|
+
prefix: serverHistory ? undefined : {
|
|
205
|
+
boundary: `PI_CODEX_REQUEST_BOUNDARY_${randomUUID()}`, contextLength: context.messages.length,
|
|
206
|
+
input: structuredClone(payload.input), contextual: payload.input.map((item) => contextual.has(item)),
|
|
207
|
+
},
|
|
172
208
|
};
|
|
173
209
|
}
|
|
174
210
|
|
|
175
|
-
/** Reuse wire
|
|
211
|
+
/** Reuse the wire prompt and request parameters only with an unchanged source prefix and configuration. */
|
|
176
212
|
export function providerRequestFor(
|
|
177
213
|
snapshots: RequestSnapshots, sessionId: string, target: CapableModel, current: readonly AgentMessage[], inputs: ProviderRequestInputs,
|
|
178
214
|
): ProviderRequestSnapshot | undefined {
|
|
179
215
|
const snapshot = snapshotFor(snapshots.providerRequest, sessionId, target);
|
|
216
|
+
const override = promptOverrideFor(snapshots.promptOverride, sessionId, target, current, inputs.systemPrompt);
|
|
217
|
+
const effectiveInputs = override ? { ...inputs, systemPrompt: override.text } : inputs;
|
|
218
|
+
const key = requestInputsKey(target, effectiveInputs);
|
|
180
219
|
return snapshot && matchingSource(current, snapshot) &&
|
|
181
|
-
snapshot.inputsKey ===
|
|
220
|
+
(snapshot.inputsKey === key || snapshot.settledInputsKey === key) ? snapshot : undefined;
|
|
182
221
|
}
|
|
183
222
|
|
|
184
|
-
/** Replace
|
|
223
|
+
/** Replace the serialized prefix up to the private boundary; never let that boundary reach the provider. */
|
|
185
224
|
export function applyProviderRequest(
|
|
186
225
|
payload: JsonObject, target: CapableModel, snapshot: ProviderRequestSnapshot | undefined,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
contextItems: ReadonlySet<JsonObject> = new Set(),
|
|
227
|
+
): { readonly payload: JsonObject; readonly contextual: readonly boolean[] } {
|
|
228
|
+
const input = Array.isArray(payload.input) && payload.input.every(isObject) ? payload.input : undefined;
|
|
229
|
+
if (!snapshot) return { payload, contextual: input?.map((item) => contextItems.has(item)) ?? [] };
|
|
230
|
+
const compatible = sameBackend(snapshot.identity, target.identity) && sameModel(snapshot.identity, target.model) && payload.model === target.model.id;
|
|
231
|
+
const prefix = snapshot.prefix;
|
|
232
|
+
if (!prefix) return { payload: compatible ? { ...payload, ...structuredClone(snapshot.fields) } : payload,
|
|
233
|
+
contextual: input?.map((item) => contextItems.has(item)) ?? [] };
|
|
234
|
+
if (!input) throw new CodexCompactionProtocolError("Prepared request cannot locate the snapshot boundary");
|
|
235
|
+
const matches = input.flatMap((item, index) => item.role === "user" && Array.isArray(item.content) &&
|
|
236
|
+
item.content.length === 1 && isObject(item.content[0]) && item.content[0].type === "input_text" &&
|
|
237
|
+
item.content[0].text === prefix.boundary ? [index] : []);
|
|
238
|
+
if (matches.length !== 1) throw new CodexCompactionProtocolError("Prepared request must contain exactly one snapshot boundary");
|
|
239
|
+
const boundaryIndex = matches[0];
|
|
240
|
+
const suffix = input.slice(boundaryIndex + 1);
|
|
241
|
+
if (!compatible) {
|
|
242
|
+
const unmarked = [...input.slice(0, boundaryIndex), ...suffix];
|
|
243
|
+
return { payload: { ...payload, input: unmarked }, contextual: unmarked.map((item) => contextItems.has(item)) };
|
|
197
244
|
}
|
|
198
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
payload: { ...payload, ...structuredClone(snapshot.fields), input: [...structuredClone(prefix.input), ...suffix] },
|
|
247
|
+
contextual: [...prefix.contextual, ...suffix.map((item) => contextItems.has(item))],
|
|
248
|
+
};
|
|
199
249
|
}
|
|
200
250
|
|
|
201
251
|
/** The snapshots that the latest ordinary request left for compaction. */
|
|
@@ -215,7 +265,8 @@ interface TrackedSnapshots {
|
|
|
215
265
|
|
|
216
266
|
/**
|
|
217
267
|
* Follow one ordinary request through Pi's hooks: `context`, then `context_with_system`, then
|
|
218
|
-
* `before_provider_request` for each provider attempt
|
|
268
|
+
* `before_provider_request` for each provider attempt, then `agent_settled` when its run ends.
|
|
269
|
+
* State lives only as long as the Pi process.
|
|
219
270
|
*/
|
|
220
271
|
export class RequestSnapshotTracker {
|
|
221
272
|
private state: TrackedSnapshots = {};
|
|
@@ -274,9 +325,23 @@ export class RequestSnapshotTracker {
|
|
|
274
325
|
this.state.providerRequest = captureProviderRequest(context, target, payload, inputs);
|
|
275
326
|
}
|
|
276
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* `agent_settled`: Pi drops the run's prompt options, so `ctx.getSystemPrompt()` reports the base prompt
|
|
331
|
+
* until the next run. Accept that prompt for the run's last request only while every other input is unchanged;
|
|
332
|
+
* the request prefix still comes from the transcript the snapshot's source fingerprints bind.
|
|
333
|
+
*/
|
|
334
|
+
recordSettledRun(sessionId: string, target: CapableModel | undefined, inputs: () => ProviderRequestInputs): void {
|
|
335
|
+
if (!target) return;
|
|
336
|
+
const snapshot = snapshotFor(this.state.providerRequest, sessionId, target);
|
|
337
|
+
if (!snapshot || snapshot.settledInputsKey !== undefined) return;
|
|
338
|
+
const settled = inputs();
|
|
339
|
+
if (requestInputsKey(target, { ...settled, systemPrompt: snapshot.systemPrompt }) !== snapshot.inputsKey) return;
|
|
340
|
+
this.state.providerRequest = { ...snapshot, settledInputsKey: requestInputsKey(target, settled) };
|
|
341
|
+
}
|
|
277
342
|
}
|
|
278
343
|
|
|
279
|
-
/**
|
|
344
|
+
/** Pi's internal declarations let the provider serialize historical grammar tool calls correctly. */
|
|
280
345
|
export interface RequestDeclarations {
|
|
281
346
|
readonly blockImages: boolean;
|
|
282
347
|
readonly systemPrompt: () => string;
|
|
@@ -298,8 +363,7 @@ function withoutImages(messages: Message[]): Message[] {
|
|
|
298
363
|
|
|
299
364
|
/**
|
|
300
365
|
* Build compaction's provider context from the current transcript and the snapshots bound to this
|
|
301
|
-
* session, model, and backend.
|
|
302
|
-
* messages, as ordinary requests do; `messages` is the transcript actually sent.
|
|
366
|
+
* session, model, and backend. Preserve tool declarations for the request prefix and historical-call serialization.
|
|
303
367
|
*/
|
|
304
368
|
export function compactionRequest(
|
|
305
369
|
snapshots: RequestSnapshots,
|
|
@@ -307,9 +371,18 @@ export function compactionRequest(
|
|
|
307
371
|
target: CapableModel,
|
|
308
372
|
current: AgentMessage[],
|
|
309
373
|
declarations: RequestDeclarations,
|
|
374
|
+
providerRequest?: ProviderRequestSnapshot,
|
|
310
375
|
): { readonly context: Context; readonly messages: AgentMessage[] } {
|
|
311
|
-
const override = promptOverrideFor(snapshots.promptOverride, sessionId, target, current);
|
|
312
|
-
const
|
|
376
|
+
const override = snapshots.promptOverride && promptOverrideFor(snapshots.promptOverride, sessionId, target, current, declarations.systemPrompt());
|
|
377
|
+
const reused = reuseContextSnapshot(current, snapshotFor(snapshots.context, sessionId, target));
|
|
378
|
+
// The provider serializes the full transcript so grammar declarations and historical IDs keep their context.
|
|
379
|
+
// This private user item marks the join; applyProviderRequest removes it and everything before it.
|
|
380
|
+
const prefix = providerRequest?.prefix;
|
|
381
|
+
const messages: AgentMessage[] = prefix ? [
|
|
382
|
+
...reused.slice(0, prefix.contextLength),
|
|
383
|
+
{ role: "user", content: prefix.boundary, timestamp: 0 },
|
|
384
|
+
...reused.slice(prefix.contextLength),
|
|
385
|
+
] : reused;
|
|
313
386
|
const converted = convertToLlm(messages);
|
|
314
387
|
const llmMessages = declarations.blockImages ? withoutImages(converted) : converted;
|
|
315
388
|
const overridden = override && applyPromptOverride(llmMessages, override);
|