@narumitw/pi-codex-compact 0.51.3 → 0.52.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/README.md +82 -70
- package/dist/chunks/chunk-JYXZMCXD.js +43 -0
- package/dist/chunks/chunk-JYXZMCXD.js.map +7 -0
- package/dist/chunks/{settings-menu-PEDLURUH.js → settings-menu-VYQ6ABCP.js} +54 -27
- package/dist/chunks/settings-menu-VYQ6ABCP.js.map +7 -0
- package/dist/index.ts +515 -72
- package/dist/index.ts.map +4 -4
- package/package.json +7 -10
- package/src/checkpoint.ts +48 -13
- package/src/codex-compact.ts +60 -33
- package/src/model-api.ts +43 -3
- package/src/protocol.ts +162 -4
- package/src/remote-compact.ts +298 -0
- package/src/remote-shared.ts +19 -0
- package/src/remote-types.ts +45 -0
- package/src/remote-v2.ts +70 -0
- package/src/remote.ts +12 -112
- package/src/settings-menu.ts +55 -20
- package/src/settings.ts +15 -0
- package/src/terminal.ts +6 -0
- package/dist/chunks/chunk-6TZ2L2BM.js +0 -13
- package/dist/chunks/chunk-6TZ2L2BM.js.map +0 -7
- package/dist/chunks/settings-menu-PEDLURUH.js.map +0 -7
package/src/codex-compact.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
latestCheckpoint,
|
|
19
19
|
projectCheckpointContext,
|
|
20
20
|
} from "./checkpoint.js";
|
|
21
|
-
import {
|
|
21
|
+
import { resolveCompactionRoute, usesResponsesCompactionApi } from "./model-api.js";
|
|
22
22
|
import { hasCheckpointMarker, rewriteCheckpointMarker } from "./protocol.js";
|
|
23
23
|
import { requestRemoteCompaction } from "./remote.js";
|
|
24
24
|
import {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type CodexCompactSettingsState,
|
|
28
28
|
createCodexCompactSettingsRuntime,
|
|
29
29
|
} from "./settings.js";
|
|
30
|
+
import { terminalText } from "./terminal.js";
|
|
30
31
|
|
|
31
32
|
const STATUS_KEY = "codex-compact";
|
|
32
33
|
|
|
@@ -37,8 +38,10 @@ function activeCheckpoint(ctx: ExtensionContext) {
|
|
|
37
38
|
function isCheckpointCompatible(
|
|
38
39
|
details: CodexCheckpointDetails,
|
|
39
40
|
model: Model<Api> | undefined,
|
|
40
|
-
):
|
|
41
|
-
return
|
|
41
|
+
): boolean {
|
|
42
|
+
return (
|
|
43
|
+
usesResponsesCompactionApi(model) && model.api === details.api && model.id === details.modelId
|
|
44
|
+
);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function keptMessages(event: SessionBeforeCompactEvent): AgentMessage[] {
|
|
@@ -54,27 +57,31 @@ function keptMessages(event: SessionBeforeCompactEvent): AgentMessage[] {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function activeTools(pi: ExtensionAPI): Tool[] {
|
|
57
|
-
const
|
|
58
|
-
return pi
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
});
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
function projectedCurrentMessages(
|
|
69
76
|
event: SessionBeforeCompactEvent,
|
|
70
|
-
model: Model<
|
|
77
|
+
model: Model<Api>,
|
|
71
78
|
): { messages: AgentMessage[]; prior?: CodexCheckpointDetails } {
|
|
72
79
|
const leafId = event.branchEntries.at(-1)?.id ?? null;
|
|
73
80
|
const session = buildSessionContext(event.branchEntries, leafId);
|
|
74
81
|
const prior = latestCheckpoint(event.branchEntries);
|
|
75
82
|
if (!prior) return { messages: session.messages };
|
|
76
|
-
if (prior.details.modelId !== model.id) {
|
|
77
|
-
throw new Error("The active opaque checkpoint belongs to a different
|
|
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");
|
|
78
85
|
}
|
|
79
86
|
const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
|
|
80
87
|
if (!projected) {
|
|
@@ -89,8 +96,8 @@ function notifyFailure(
|
|
|
89
96
|
settings: CodexCompactSettings,
|
|
90
97
|
): void {
|
|
91
98
|
if (!ctx.hasUI || !settings.notifyOnFallback) return;
|
|
92
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
-
ctx.ui.notify(`
|
|
99
|
+
const message = terminalText(error instanceof Error ? error.message : String(error));
|
|
100
|
+
ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
function sessionStillOwned(ctx: ExtensionContext, sessionId: string, signal: AbortSignal): boolean {
|
|
@@ -102,18 +109,25 @@ async function compactRemotely(
|
|
|
102
109
|
event: SessionBeforeCompactEvent,
|
|
103
110
|
ctx: ExtensionContext,
|
|
104
111
|
settings: CodexCompactSettings,
|
|
112
|
+
ownerSignal: AbortSignal,
|
|
105
113
|
fetch?: typeof globalThis.fetch,
|
|
106
114
|
) {
|
|
107
115
|
const model = ctx.model;
|
|
108
|
-
|
|
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 };
|
|
109
120
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
110
|
-
ctx.ui.setStatus(
|
|
121
|
+
ctx.ui.setStatus(
|
|
122
|
+
STATUS_KEY,
|
|
123
|
+
route.protocol === "remote-v2" ? "Responses Remote V2…" : "Responses Compact API…",
|
|
124
|
+
);
|
|
111
125
|
try {
|
|
112
126
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
113
|
-
if (!sessionStillOwned(ctx, sessionId,
|
|
127
|
+
if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
|
|
114
128
|
if (!auth.ok) throw new Error(auth.error);
|
|
115
129
|
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
116
|
-
if (!provider) throw new Error("The active
|
|
130
|
+
if (!provider) throw new Error("The active Responses provider is unavailable");
|
|
117
131
|
const current = projectedCurrentMessages(event, model);
|
|
118
132
|
const context: Context = {
|
|
119
133
|
systemPrompt: ctx.getSystemPrompt(),
|
|
@@ -124,10 +138,11 @@ async function compactRemotely(
|
|
|
124
138
|
provider,
|
|
125
139
|
model,
|
|
126
140
|
context,
|
|
141
|
+
protocol: route.protocol,
|
|
127
142
|
apiKey: auth.apiKey,
|
|
128
143
|
headers: auth.headers,
|
|
129
144
|
env: auth.env,
|
|
130
|
-
signal
|
|
145
|
+
signal,
|
|
131
146
|
priorCheckpoint: current.prior
|
|
132
147
|
? {
|
|
133
148
|
marker: checkpointMarker(current.prior.checkpointId),
|
|
@@ -138,13 +153,17 @@ async function compactRemotely(
|
|
|
138
153
|
maxRetries: settings.maxRetries,
|
|
139
154
|
fetch,
|
|
140
155
|
});
|
|
141
|
-
if (!sessionStillOwned(ctx, sessionId,
|
|
142
|
-
const replacementHistory = buildReplacementHistory(
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
);
|
|
145
162
|
const details = createCheckpointDetails({
|
|
146
163
|
provider: model.provider,
|
|
164
|
+
api: route.api,
|
|
147
165
|
modelId: model.id,
|
|
166
|
+
protocol: route.protocol,
|
|
148
167
|
replacementHistory,
|
|
149
168
|
keptMessages: keptMessages(event),
|
|
150
169
|
});
|
|
@@ -158,7 +177,7 @@ async function compactRemotely(
|
|
|
158
177
|
},
|
|
159
178
|
};
|
|
160
179
|
} catch (error) {
|
|
161
|
-
if (
|
|
180
|
+
if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
|
|
162
181
|
return { cancel: true };
|
|
163
182
|
}
|
|
164
183
|
notifyFailure(ctx, error, settings);
|
|
@@ -178,8 +197,9 @@ export function createCodexCompactExtension(
|
|
|
178
197
|
let generation = 0;
|
|
179
198
|
|
|
180
199
|
pi.registerCommand("codex-compact", {
|
|
181
|
-
description: "Compact now or configure
|
|
182
|
-
handler: async (
|
|
200
|
+
description: "Compact now or configure Responses compaction",
|
|
201
|
+
handler: async (args, ctx) => {
|
|
202
|
+
if (args.trim()) throw new Error("Usage: /codex-compact");
|
|
183
203
|
const ownerGeneration = generation;
|
|
184
204
|
const controller = sessionController;
|
|
185
205
|
const { showCodexCompactMenu } = await import("./settings-menu.js");
|
|
@@ -205,7 +225,7 @@ export function createCodexCompactExtension(
|
|
|
205
225
|
if (sessionController.signal.aborted || ownerGeneration !== generation) return;
|
|
206
226
|
if (ctx.hasUI) {
|
|
207
227
|
ctx.ui.notify(
|
|
208
|
-
`Could not load pi-codex-compact.json; using defaults. ${error instanceof Error ? error.message : String(error)}`,
|
|
228
|
+
`Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
|
|
209
229
|
"warning",
|
|
210
230
|
);
|
|
211
231
|
}
|
|
@@ -220,14 +240,21 @@ export function createCodexCompactExtension(
|
|
|
220
240
|
}
|
|
221
241
|
if (ctx.hasUI && state.kind === "invalid") {
|
|
222
242
|
ctx.ui.notify(
|
|
223
|
-
`Invalid pi-codex-compact.json; using defaults without overwriting it. ${state.issue}`,
|
|
243
|
+
`Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
|
|
224
244
|
"warning",
|
|
225
245
|
);
|
|
226
246
|
}
|
|
227
247
|
});
|
|
228
248
|
|
|
229
249
|
pi.on("session_before_compact", (event, ctx) =>
|
|
230
|
-
compactRemotely(
|
|
250
|
+
compactRemotely(
|
|
251
|
+
pi,
|
|
252
|
+
event,
|
|
253
|
+
ctx,
|
|
254
|
+
settingsRuntime.get().settings,
|
|
255
|
+
sessionController.signal,
|
|
256
|
+
options.fetch,
|
|
257
|
+
),
|
|
231
258
|
);
|
|
232
259
|
|
|
233
260
|
pi.on("context", (event, ctx) => {
|
|
@@ -260,7 +287,7 @@ export function createCodexCompactExtension(
|
|
|
260
287
|
providerWarnings.add(key);
|
|
261
288
|
if (ctx.hasUI) {
|
|
262
289
|
ctx.ui.notify(
|
|
263
|
-
"The active
|
|
290
|
+
"The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
|
|
264
291
|
"warning",
|
|
265
292
|
);
|
|
266
293
|
}
|
package/src/model-api.ts
CHANGED
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { hasApi } from "@earendil-works/pi-ai";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export const RESPONSES_COMPACTION_APIS = [
|
|
5
|
+
"openai-codex-responses",
|
|
6
|
+
"openai-responses",
|
|
7
|
+
"azure-openai-responses",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type ResponsesCompactionApi = (typeof RESPONSES_COMPACTION_APIS)[number];
|
|
11
|
+
export type RemoteCompactionProtocol = "remote-v2" | "responses-compact";
|
|
12
|
+
export type RemoteCompactionProtocolSetting = "auto" | RemoteCompactionProtocol;
|
|
13
|
+
|
|
14
|
+
export type CompactionRoute =
|
|
15
|
+
| { kind: "remote"; protocol: RemoteCompactionProtocol; api: ResponsesCompactionApi }
|
|
16
|
+
| { kind: "native"; reason: string };
|
|
17
|
+
|
|
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));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCompactionRouteForApi(
|
|
25
|
+
api: Api | undefined,
|
|
26
|
+
options: { enabled: boolean; protocol: RemoteCompactionProtocolSetting },
|
|
27
|
+
): 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 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveCompactionRoute(
|
|
5
44
|
model: Model<Api> | undefined,
|
|
6
|
-
|
|
7
|
-
|
|
45
|
+
options: { enabled: boolean; protocol: RemoteCompactionProtocolSetting },
|
|
46
|
+
): CompactionRoute {
|
|
47
|
+
return resolveCompactionRouteForApi(model?.api, options);
|
|
8
48
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const MAX_SSE_BYTES = 8 * 1024 * 1024;
|
|
2
|
+
export const MAX_COMPACT_JSON_BYTES = 8 * 1024 * 1024;
|
|
2
3
|
export const MAX_COMPACTION_ITEM_BYTES = 2 * 1024 * 1024;
|
|
3
4
|
|
|
4
5
|
export type JsonObject = Record<string, unknown>;
|
|
@@ -47,6 +48,12 @@ export interface CollectedCompaction {
|
|
|
47
48
|
completedResponse?: JsonObject;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export interface CollectedCompactResponse {
|
|
52
|
+
item: JsonObject;
|
|
53
|
+
output: JsonObject[];
|
|
54
|
+
response: JsonObject;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
function compactionItemsFromEvent(event: JsonObject): unknown[] {
|
|
51
58
|
const items: unknown[] = [];
|
|
52
59
|
if (event.type === "response.output_item.done" && isObject(event.item)) {
|
|
@@ -156,6 +163,147 @@ export async function collectCompactionSse(
|
|
|
156
163
|
return { item: [...items.values()][0], completedResponse };
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
function isRetainedCompactContent(value: unknown): value is JsonObject {
|
|
167
|
+
if (!isObject(value)) return false;
|
|
168
|
+
if (value.type === "input_text") return typeof value.text === "string";
|
|
169
|
+
if (value.type !== "input_image") return false;
|
|
170
|
+
if (
|
|
171
|
+
value.detail !== undefined &&
|
|
172
|
+
value.detail !== null &&
|
|
173
|
+
value.detail !== "auto" &&
|
|
174
|
+
value.detail !== "low" &&
|
|
175
|
+
value.detail !== "high" &&
|
|
176
|
+
value.detail !== "original"
|
|
177
|
+
) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
(value.file_id !== undefined && value.file_id !== null && typeof value.file_id !== "string") ||
|
|
182
|
+
(value.image_url !== undefined &&
|
|
183
|
+
value.image_url !== null &&
|
|
184
|
+
typeof value.image_url !== "string")
|
|
185
|
+
) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
(typeof value.file_id === "string" && value.file_id.length > 0) ||
|
|
190
|
+
(typeof value.image_url === "string" && value.image_url.length > 0)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isRetainedCompactMessage(value: unknown): value is JsonObject {
|
|
195
|
+
return (
|
|
196
|
+
isObject(value) &&
|
|
197
|
+
value.role === "user" &&
|
|
198
|
+
(value.type === undefined || value.type === "message") &&
|
|
199
|
+
Array.isArray(value.content) &&
|
|
200
|
+
value.content.length > 0 &&
|
|
201
|
+
value.content.every(isRetainedCompactContent)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function validateCompactedResponse(
|
|
206
|
+
value: unknown,
|
|
207
|
+
options: { maxBytes?: number; maxItemBytes?: number } = {},
|
|
208
|
+
): CollectedCompactResponse {
|
|
209
|
+
if (!isObject(value) || !Array.isArray(value.output)) {
|
|
210
|
+
throw new CodexCompactionProtocolError("Responses Compact returned an invalid response object");
|
|
211
|
+
}
|
|
212
|
+
const maxBytes = options.maxBytes ?? MAX_COMPACT_JSON_BYTES;
|
|
213
|
+
const maxItemBytes = options.maxItemBytes ?? MAX_COMPACTION_ITEM_BYTES;
|
|
214
|
+
if (byteLength(value) > maxBytes) {
|
|
215
|
+
throw new CodexCompactionProtocolError("Responses Compact response exceeded the size limit");
|
|
216
|
+
}
|
|
217
|
+
if (value.output.length === 0) {
|
|
218
|
+
throw new CodexCompactionProtocolError("Responses Compact returned no output items");
|
|
219
|
+
}
|
|
220
|
+
const output = value.output.map((item) => {
|
|
221
|
+
if (!isObject(item)) {
|
|
222
|
+
throw new CodexCompactionProtocolError("Responses Compact returned a non-object output item");
|
|
223
|
+
}
|
|
224
|
+
if (byteLength(item) > maxItemBytes) {
|
|
225
|
+
throw new CodexCompactionProtocolError(
|
|
226
|
+
"Responses Compact output item exceeded the size limit",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return structuredClone(item);
|
|
230
|
+
});
|
|
231
|
+
const compactionItems = output.filter((item) => item.type === "compaction");
|
|
232
|
+
if (compactionItems.length !== 1 || output.at(-1)?.type !== "compaction") {
|
|
233
|
+
throw new CodexCompactionProtocolError(
|
|
234
|
+
"Responses Compact must return retained messages followed by one compaction item",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (const item of output.slice(0, -1)) {
|
|
238
|
+
if (!isRetainedCompactMessage(item)) {
|
|
239
|
+
throw new CodexCompactionProtocolError(
|
|
240
|
+
"Responses Compact returned an unsupported retained output item",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const item = validateCompactionItem(output.at(-1), maxItemBytes);
|
|
245
|
+
return { item, output: [...output.slice(0, -1), item], response: structuredClone(value) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function collectCompactResponse(
|
|
249
|
+
response: Response,
|
|
250
|
+
options: { signal?: AbortSignal; maxBytes?: number; maxItemBytes?: number } = {},
|
|
251
|
+
): Promise<CollectedCompactResponse> {
|
|
252
|
+
const maxBytes = options.maxBytes ?? MAX_COMPACT_JSON_BYTES;
|
|
253
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
254
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
255
|
+
const error = new CodexCompactionProtocolError(
|
|
256
|
+
"Responses Compact response exceeded the size limit",
|
|
257
|
+
);
|
|
258
|
+
await response.body?.cancel(error).catch(() => undefined);
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
if (!response.body) {
|
|
262
|
+
throw new CodexCompactionProtocolError("Responses Compact response did not contain a body");
|
|
263
|
+
}
|
|
264
|
+
const reader = response.body.getReader();
|
|
265
|
+
const chunks: Uint8Array[] = [];
|
|
266
|
+
let bytes = 0;
|
|
267
|
+
const onAbort = () => {
|
|
268
|
+
void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => undefined);
|
|
269
|
+
};
|
|
270
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
271
|
+
try {
|
|
272
|
+
while (true) {
|
|
273
|
+
if (options.signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
if (done) break;
|
|
276
|
+
bytes += value.byteLength;
|
|
277
|
+
if (bytes > maxBytes) {
|
|
278
|
+
throw new CodexCompactionProtocolError(
|
|
279
|
+
"Responses Compact response exceeded the size limit",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
chunks.push(value);
|
|
283
|
+
}
|
|
284
|
+
if (options.signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
285
|
+
} catch (error) {
|
|
286
|
+
await reader.cancel(error).catch(() => undefined);
|
|
287
|
+
throw error;
|
|
288
|
+
} finally {
|
|
289
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
290
|
+
reader.releaseLock();
|
|
291
|
+
}
|
|
292
|
+
const body = new Uint8Array(bytes);
|
|
293
|
+
let offset = 0;
|
|
294
|
+
for (const chunk of chunks) {
|
|
295
|
+
body.set(chunk, offset);
|
|
296
|
+
offset += chunk.byteLength;
|
|
297
|
+
}
|
|
298
|
+
let parsed: unknown;
|
|
299
|
+
try {
|
|
300
|
+
parsed = JSON.parse(new TextDecoder().decode(body));
|
|
301
|
+
} catch {
|
|
302
|
+
throw new CodexCompactionProtocolError("Responses Compact returned malformed JSON");
|
|
303
|
+
}
|
|
304
|
+
return validateCompactedResponse(parsed, options);
|
|
305
|
+
}
|
|
306
|
+
|
|
159
307
|
function markerTextFromItem(item: unknown): string | undefined {
|
|
160
308
|
if (!isObject(item) || item.role !== "user" || !Array.isArray(item.content)) return undefined;
|
|
161
309
|
if (item.content.length !== 1) return undefined;
|
|
@@ -205,14 +353,24 @@ export function appendCompactionTrigger(payload: unknown): JsonObject {
|
|
|
205
353
|
return { ...payload, input: [...payload.input, { type: "compaction_trigger" }] };
|
|
206
354
|
}
|
|
207
355
|
|
|
356
|
+
export function expandRemoteCompactionPayload(
|
|
357
|
+
payload: unknown,
|
|
358
|
+
checkpoint?: { marker: string; replacementHistory: readonly unknown[] },
|
|
359
|
+
): JsonObject {
|
|
360
|
+
if (checkpoint) {
|
|
361
|
+
return rewriteCheckpointMarker(payload, checkpoint.marker, checkpoint.replacementHistory);
|
|
362
|
+
}
|
|
363
|
+
if (!isObject(payload) || !Array.isArray(payload.input)) {
|
|
364
|
+
throw new CodexCompactionProtocolError("Responses payload is missing an input array");
|
|
365
|
+
}
|
|
366
|
+
return structuredClone(payload);
|
|
367
|
+
}
|
|
368
|
+
|
|
208
369
|
export function prepareRemoteCompactionPayload(
|
|
209
370
|
payload: unknown,
|
|
210
371
|
checkpoint?: { marker: string; replacementHistory: readonly unknown[] },
|
|
211
372
|
): JsonObject {
|
|
212
|
-
|
|
213
|
-
? rewriteCheckpointMarker(payload, checkpoint.marker, checkpoint.replacementHistory)
|
|
214
|
-
: payload;
|
|
215
|
-
return appendCompactionTrigger(expanded);
|
|
373
|
+
return appendCompactionTrigger(expandRemoteCompactionPayload(payload, checkpoint));
|
|
216
374
|
}
|
|
217
375
|
|
|
218
376
|
export function hasCheckpointMarker(payload: unknown, marker: string): boolean {
|