@gajae-code/agent-core 0.1.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +482 -0
  2. package/README.md +473 -0
  3. package/dist/types/agent-loop.d.ts +55 -0
  4. package/dist/types/agent.d.ts +334 -0
  5. package/dist/types/append-only-context.d.ts +113 -0
  6. package/dist/types/compaction/branch-summarization.d.ts +94 -0
  7. package/dist/types/compaction/compaction.d.ts +166 -0
  8. package/dist/types/compaction/entries.d.ts +103 -0
  9. package/dist/types/compaction/errors.d.ts +26 -0
  10. package/dist/types/compaction/index.d.ts +11 -0
  11. package/dist/types/compaction/messages.d.ts +61 -0
  12. package/dist/types/compaction/openai.d.ts +58 -0
  13. package/dist/types/compaction/pruning.d.ts +18 -0
  14. package/dist/types/compaction/utils.d.ts +32 -0
  15. package/dist/types/compaction.d.ts +1 -0
  16. package/dist/types/harmony-leak.d.ts +99 -0
  17. package/dist/types/index.d.ts +10 -0
  18. package/dist/types/proxy.d.ts +84 -0
  19. package/dist/types/run-collector.d.ts +196 -0
  20. package/dist/types/telemetry.d.ts +588 -0
  21. package/dist/types/thinking.d.ts +17 -0
  22. package/dist/types/types.d.ts +407 -0
  23. package/package.json +75 -0
  24. package/src/agent-loop.ts +1279 -0
  25. package/src/agent.ts +1399 -0
  26. package/src/append-only-context.ts +297 -0
  27. package/src/compaction/branch-summarization.ts +339 -0
  28. package/src/compaction/compaction.ts +1065 -0
  29. package/src/compaction/entries.ts +133 -0
  30. package/src/compaction/errors.ts +31 -0
  31. package/src/compaction/index.ts +12 -0
  32. package/src/compaction/messages.ts +212 -0
  33. package/src/compaction/openai.ts +552 -0
  34. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  35. package/src/compaction/prompts/branch-summary-context.md +5 -0
  36. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  37. package/src/compaction/prompts/branch-summary.md +30 -0
  38. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  39. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  40. package/src/compaction/prompts/compaction-summary.md +38 -0
  41. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  42. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  43. package/src/compaction/prompts/file-operations.md +10 -0
  44. package/src/compaction/prompts/handoff-document.md +49 -0
  45. package/src/compaction/prompts/summarization-system.md +3 -0
  46. package/src/compaction/pruning.ts +92 -0
  47. package/src/compaction/utils.ts +185 -0
  48. package/src/compaction.ts +1 -0
  49. package/src/harmony-leak.ts +427 -0
  50. package/src/index.ts +19 -0
  51. package/src/proxy.ts +326 -0
  52. package/src/run-collector.ts +631 -0
  53. package/src/telemetry.ts +2018 -0
  54. package/src/thinking.ts +19 -0
  55. package/src/types.ts +467 -0
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Remote compaction utilities.
3
+ *
4
+ * Provider-side conversation summarization endpoints. Two flavors:
5
+ *
6
+ * - **OpenAI remote compaction** (`/responses/compact`): preserves encrypted
7
+ * reasoning across compactions by submitting the full responses-API native
8
+ * history and storing the returned `compaction` / `compaction_summary`
9
+ * item in `preserveData` so future turns can replay the encrypted state.
10
+ * - **Generic remote compaction**: a thin POST helper for self-hosted
11
+ * summarization endpoints that accept `{ systemPrompt, prompt }` and reply
12
+ * with `{ summary, shortSummary? }`.
13
+ */
14
+
15
+ import {
16
+ CODEX_BASE_URL,
17
+ getCodexAccountId,
18
+ OPENAI_HEADER_VALUES,
19
+ OPENAI_HEADERS,
20
+ } from "@gajae-code/ai/providers/openai-codex/constants";
21
+ import { parseTextSignature } from "@gajae-code/ai/providers/openai-responses-shared";
22
+ import { transformMessages } from "@gajae-code/ai/providers/transform-messages";
23
+ import type { AssistantMessage, Message, Model } from "@gajae-code/ai/types";
24
+ import {
25
+ getOpenAIResponsesHistoryItems,
26
+ getOpenAIResponsesHistoryPayload,
27
+ normalizeResponsesToolCallId,
28
+ } from "@gajae-code/ai/utils";
29
+ import { logger } from "@gajae-code/utils";
30
+
31
+ // ============================================================================
32
+ // Public types
33
+ // ============================================================================
34
+
35
+ export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
36
+
37
+ export type OpenAiRemoteCompactionItem = {
38
+ type: "compaction" | "compaction_summary";
39
+ encrypted_content?: string;
40
+ summary?: string;
41
+ };
42
+
43
+ export interface OpenAiRemoteCompactionPreserveData {
44
+ provider?: string;
45
+ replacementHistory: Array<Record<string, unknown>>;
46
+ compactionItem: OpenAiRemoteCompactionItem;
47
+ }
48
+
49
+ export interface OpenAiRemoteCompactionRequest {
50
+ model: string;
51
+ input: Array<Record<string, unknown>>;
52
+ instructions: string;
53
+ }
54
+
55
+ export interface OpenAiRemoteCompactionResponse extends OpenAiRemoteCompactionPreserveData {}
56
+
57
+ export interface RemoteCompactionRequest {
58
+ systemPrompt: string;
59
+ prompt: string;
60
+ }
61
+
62
+ export interface RemoteCompactionResponse {
63
+ summary: string;
64
+ shortSummary?: string;
65
+ }
66
+
67
+ // ============================================================================
68
+ // OpenAI provider gating + endpoint resolution
69
+ // ============================================================================
70
+
71
+ export function shouldUseOpenAiRemoteCompaction(model: Model): boolean {
72
+ return model.provider === "openai" || model.provider === "openai-codex";
73
+ }
74
+
75
+ function resolveOpenAiCompactEndpoint(model: Model): string {
76
+ if (model.provider === "openai-codex") {
77
+ return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
78
+ }
79
+
80
+ const defaultBase = "https://api.openai.com/v1";
81
+ const rawBase = model.baseUrl && model.baseUrl.length > 0 ? model.baseUrl : defaultBase;
82
+ const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
83
+ if (normalizedBase.endsWith("/v1")) return `${normalizedBase}/responses/compact`;
84
+ return `${normalizedBase}/v1/responses/compact`;
85
+ }
86
+
87
+ function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
88
+ const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
89
+ const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
90
+ if (/\/codex(?:\/v\d+)?$/.test(normalizedBase)) return `${normalizedBase}/responses/compact`;
91
+ return `${normalizedBase}/codex/responses/compact`;
92
+ }
93
+
94
+ function normalizeOpenAiCompactionToolCallId(id: string): string {
95
+ const normalized = normalizeResponsesToolCallId(id);
96
+ return `${normalized.callId}|${normalized.itemId ?? normalized.callId}`;
97
+ }
98
+
99
+ // ============================================================================
100
+ // Preserve-data helpers
101
+ // ============================================================================
102
+
103
+ export function getPreservedOpenAiRemoteCompactionData(
104
+ preserveData: Record<string, unknown> | undefined,
105
+ ): OpenAiRemoteCompactionPreserveData | undefined {
106
+ const candidate = preserveData?.[OPENAI_REMOTE_COMPACTION_PRESERVE_KEY];
107
+ if (!candidate || typeof candidate !== "object") return undefined;
108
+ const maybeData = candidate as { provider?: unknown; replacementHistory?: unknown; compactionItem?: unknown };
109
+ if (!Array.isArray(maybeData.replacementHistory)) return undefined;
110
+ const maybeItem = maybeData.compactionItem;
111
+ if (!maybeItem || typeof maybeItem !== "object") return undefined;
112
+ const compactionItem = maybeItem as { type?: unknown; encrypted_content?: unknown; summary?: unknown };
113
+ const isClassicCompaction =
114
+ compactionItem.type === "compaction" && typeof compactionItem.encrypted_content === "string";
115
+ const isSummaryCompaction = compactionItem.type === "compaction_summary";
116
+ if (!isClassicCompaction && !isSummaryCompaction) {
117
+ return undefined;
118
+ }
119
+ return {
120
+ provider: typeof maybeData.provider === "string" ? maybeData.provider : undefined,
121
+ replacementHistory: maybeData.replacementHistory as Array<Record<string, unknown>>,
122
+ compactionItem: compactionItem as unknown as OpenAiRemoteCompactionItem,
123
+ };
124
+ }
125
+
126
+ export function withOpenAiRemoteCompactionPreserveData(
127
+ preserveData: Record<string, unknown> | undefined,
128
+ remoteCompaction: OpenAiRemoteCompactionPreserveData | undefined,
129
+ ): Record<string, unknown> | undefined {
130
+ if (remoteCompaction) {
131
+ return {
132
+ ...(preserveData ?? {}),
133
+ [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: remoteCompaction,
134
+ };
135
+ }
136
+
137
+ if (!preserveData || !(OPENAI_REMOTE_COMPACTION_PRESERVE_KEY in preserveData)) {
138
+ return preserveData;
139
+ }
140
+
141
+ const { [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: _removed, ...rest } = preserveData;
142
+ return Object.keys(rest).length > 0 ? rest : undefined;
143
+ }
144
+
145
+ // ============================================================================
146
+ // Input/output filtering for OpenAI compact endpoint
147
+ // ============================================================================
148
+
149
+ function estimateOpenAiCompactInputTokens(input: Array<Record<string, unknown>>, instructions: string): number {
150
+ let chars = instructions.length;
151
+ for (const item of input) {
152
+ chars += JSON.stringify(item).length;
153
+ }
154
+ return Math.ceil(chars / 4);
155
+ }
156
+
157
+ function shouldTrimOpenAiCompactInputItem(item: Record<string, unknown>): boolean {
158
+ return item.type === "function_call_output" || (item.type === "message" && item.role === "developer");
159
+ }
160
+
161
+ function shouldKeepOpenAiCompactOutputUserMessage(item: Record<string, unknown>): boolean {
162
+ if (item.role !== "user") return false;
163
+ const content = item.content;
164
+ if (!Array.isArray(content) || content.length === 0) return false;
165
+ const contextualFragmentPatterns = [
166
+ [/^<system-reminder>[\s\S]*<\/system-reminder>$/i, /<system-reminder>/i],
167
+ [/^#\s*AGENTS\.md instructions for\b[\s\S]*<\/INSTRUCTIONS>$/i, /# AGENTS.md instructions/],
168
+ [/^<environment-context>[\s\S]*<\/environment-context>$/i, /<environment-context>/i],
169
+ [/^<skill>[\s\S]*<\/skill>$/i, /<skill>/i],
170
+ [/^<user-shell-command>[\s\S]*<\/user-shell-command>$/i, /<user-shell-command>/i],
171
+ [/^<turn-aborted>[\s\S]*<\/turn-aborted>$/i, /<turn-aborted>/i],
172
+ [/^<subagent-notification>[\s\S]*<\/subagent-notification>$/i, /<subagent-notification>/i],
173
+ ] as const;
174
+ return content.every(part => {
175
+ if (!part || typeof part !== "object") return false;
176
+ const candidate = part as { type?: unknown; text?: unknown };
177
+ if (candidate.type === "input_image") return true;
178
+ if (candidate.type !== "input_text" || typeof candidate.text !== "string") return false;
179
+ const trimmed = candidate.text.trim();
180
+ if (trimmed.length === 0) return false;
181
+ return !contextualFragmentPatterns.some(([strictPattern, markerPattern]) => {
182
+ return strictPattern.test(trimmed) || markerPattern.test(trimmed);
183
+ });
184
+ });
185
+ }
186
+
187
+ function shouldKeepOpenAiCompactOutputItem(item: Record<string, unknown>): boolean {
188
+ if (item.type === "compaction" || item.type === "compaction_summary") return true;
189
+ if (item.type !== "message") return false;
190
+ if (item.role === "developer") return false;
191
+ if (item.role === "assistant") return true;
192
+ return shouldKeepOpenAiCompactOutputUserMessage(item);
193
+ }
194
+
195
+ function trimOpenAiCompactInput(
196
+ input: Array<Record<string, unknown>>,
197
+ contextWindow: number,
198
+ instructions: string,
199
+ ): Array<Record<string, unknown>> {
200
+ const trimmed = [...input];
201
+ while (trimmed.length > 0 && estimateOpenAiCompactInputTokens(trimmed, instructions) > contextWindow) {
202
+ const last = trimmed[trimmed.length - 1];
203
+ if (last?.type === "function_call_output" || last?.type === "custom_tool_call_output") {
204
+ const callId = typeof last.call_id === "string" ? last.call_id : undefined;
205
+ const callType = last.type === "custom_tool_call_output" ? "custom_tool_call" : "function_call";
206
+ trimmed.pop();
207
+ if (callId) {
208
+ const matchingCallIndex = trimmed.findLastIndex(item => item.type === callType && item.call_id === callId);
209
+ if (matchingCallIndex >= 0) {
210
+ trimmed.splice(matchingCallIndex, 1);
211
+ }
212
+ }
213
+ continue;
214
+ }
215
+ if (!last || !shouldTrimOpenAiCompactInputItem(last)) {
216
+ break;
217
+ }
218
+ trimmed.pop();
219
+ }
220
+ return trimmed;
221
+ }
222
+
223
+ function collectKnownOpenAiCallIds(items: Array<Record<string, unknown>>): Set<string> {
224
+ const knownCallIds = new Set<string>();
225
+ for (const item of items) {
226
+ if ((item.type === "function_call" || item.type === "custom_tool_call") && typeof item.call_id === "string") {
227
+ knownCallIds.add(item.call_id);
228
+ }
229
+ }
230
+ return knownCallIds;
231
+ }
232
+
233
+ function collectCustomOpenAiCallIds(items: Array<Record<string, unknown>>): Set<string> {
234
+ const customCallIds = new Set<string>();
235
+ for (const item of items) {
236
+ if (item.type === "custom_tool_call" && typeof item.call_id === "string") {
237
+ customCallIds.add(item.call_id);
238
+ }
239
+ }
240
+ return customCallIds;
241
+ }
242
+
243
+ // ============================================================================
244
+ // Native history construction (responses-API shape)
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Build the OpenAI Responses-API native history array from LLM messages.
249
+ *
250
+ * Caller is responsible for converting any custom message types to
251
+ * `Message[]` first (e.g. via the agent's `convertToLlm`); this function
252
+ * operates purely on the LLM-domain shape.
253
+ *
254
+ * @param messages - LLM messages to encode.
255
+ * @param model - Target model (used for provider gating + tool-call id rules).
256
+ * @param previousReplacementHistory - History from a prior compaction whose
257
+ * encrypted reasoning we want to preserve.
258
+ */
259
+ export function buildOpenAiNativeHistory(
260
+ messages: Message[],
261
+ model: Model,
262
+ previousReplacementHistory?: Array<Record<string, unknown>>,
263
+ ): Array<Record<string, unknown>> {
264
+ const input: Array<Record<string, unknown>> = previousReplacementHistory ? [...previousReplacementHistory] : [];
265
+ const transformedMessages = transformMessages(messages, model, id => normalizeOpenAiCompactionToolCallId(id));
266
+
267
+ let msgIndex = 0;
268
+ let knownCallIds = collectKnownOpenAiCallIds(input);
269
+ let customCallIds = collectCustomOpenAiCallIds(input);
270
+ for (const message of transformedMessages) {
271
+ if (message.role === "user" || message.role === "developer") {
272
+ const providerPayload = (message as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
273
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
274
+ if (historyItems) {
275
+ input.push(...historyItems);
276
+ knownCallIds = collectKnownOpenAiCallIds(input);
277
+ customCallIds = collectCustomOpenAiCallIds(input);
278
+ msgIndex++;
279
+ continue;
280
+ }
281
+
282
+ const contentBlocks: Array<Record<string, unknown>> = [];
283
+ if (typeof message.content === "string") {
284
+ if (message.content.trim().length > 0) {
285
+ contentBlocks.push({ type: "input_text", text: message.content.toWellFormed() });
286
+ }
287
+ } else {
288
+ for (const block of message.content) {
289
+ if (block.type === "text") {
290
+ if (!block.text || block.text.trim().length === 0) continue;
291
+ contentBlocks.push({ type: "input_text", text: block.text.toWellFormed() });
292
+ continue;
293
+ }
294
+ if (block.type === "image") {
295
+ contentBlocks.push({
296
+ type: "input_image",
297
+ detail: "auto",
298
+ image_url: `data:${block.mimeType};base64,${block.data}`,
299
+ });
300
+ }
301
+ }
302
+ }
303
+ if (contentBlocks.length > 0) {
304
+ input.push({ type: "message", role: message.role, content: contentBlocks });
305
+ }
306
+ msgIndex++;
307
+ continue;
308
+ }
309
+
310
+ if (message.role === "assistant") {
311
+ const assistant = message as AssistantMessage;
312
+ const providerPayload = getOpenAIResponsesHistoryPayload(
313
+ assistant.providerPayload,
314
+ model.provider,
315
+ assistant.provider,
316
+ );
317
+ if (providerPayload) {
318
+ if (providerPayload.dt) {
319
+ input.push(...providerPayload.items);
320
+ } else {
321
+ input.splice(0, input.length, ...providerPayload.items);
322
+ }
323
+ knownCallIds = collectKnownOpenAiCallIds(input);
324
+ customCallIds = collectCustomOpenAiCallIds(input);
325
+ msgIndex++;
326
+ continue;
327
+ }
328
+ const isDifferentModel =
329
+ assistant.model !== model.id && assistant.provider === model.provider && assistant.api === model.api;
330
+
331
+ for (const block of assistant.content) {
332
+ if (block.type === "thinking" && assistant.stopReason !== "error" && block.thinkingSignature) {
333
+ try {
334
+ const reasoningItem = JSON.parse(block.thinkingSignature) as Record<string, unknown>;
335
+ if (reasoningItem && typeof reasoningItem === "object") {
336
+ input.push(reasoningItem);
337
+ }
338
+ } catch {
339
+ logger.warn("Failed to parse assistant reasoning for remote compaction", {
340
+ model: assistant.model,
341
+ provider: assistant.provider,
342
+ });
343
+ }
344
+ continue;
345
+ }
346
+
347
+ if (block.type === "text") {
348
+ if (!block.text || block.text.trim().length === 0) continue;
349
+ const parsedSignature = parseTextSignature(block.textSignature);
350
+ let msgId = parsedSignature?.id;
351
+ if (!msgId) {
352
+ msgId = `msg_${msgIndex}`;
353
+ } else if (msgId.length > 64) {
354
+ msgId = `msg_${Bun.hash(msgId).toString(36)}`;
355
+ }
356
+ input.push({
357
+ type: "message",
358
+ role: "assistant",
359
+ content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
360
+ status: "completed",
361
+ id: msgId,
362
+ phase: parsedSignature?.phase,
363
+ });
364
+ continue;
365
+ }
366
+
367
+ if (block.type === "toolCall") {
368
+ const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
369
+ let itemId: string | undefined = normalized.itemId;
370
+ if (
371
+ isDifferentModel &&
372
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
373
+ ) {
374
+ itemId = undefined;
375
+ }
376
+ knownCallIds.add(normalized.callId);
377
+ if (block.customWireName) {
378
+ const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
379
+ customCallIds.add(normalized.callId);
380
+ input.push({
381
+ type: "custom_tool_call",
382
+ id: itemId,
383
+ call_id: normalized.callId,
384
+ name: block.customWireName,
385
+ input: rawInput,
386
+ });
387
+ continue;
388
+ }
389
+ input.push({
390
+ type: "function_call",
391
+ id: itemId,
392
+ call_id: normalized.callId,
393
+ name: block.name,
394
+ arguments: JSON.stringify(block.arguments),
395
+ });
396
+ }
397
+ }
398
+
399
+ msgIndex++;
400
+ continue;
401
+ }
402
+
403
+ if (message.role === "toolResult") {
404
+ const normalized = normalizeResponsesToolCallId(message.toolCallId);
405
+ if (!knownCallIds.has(normalized.callId)) {
406
+ msgIndex++;
407
+ continue;
408
+ }
409
+
410
+ const textOutput = message.content
411
+ .filter(block => block.type === "text")
412
+ .map(block => block.text)
413
+ .join("\n");
414
+ const hasImages = message.content.some(block => block.type === "image");
415
+ const outputText = textOutput.length > 0 ? textOutput : hasImages ? "(see attached image)" : "";
416
+ input.push({
417
+ type: customCallIds.has(normalized.callId) ? "custom_tool_call_output" : "function_call_output",
418
+ call_id: normalized.callId,
419
+ output: outputText.toWellFormed(),
420
+ });
421
+
422
+ if (hasImages && model.input.includes("image")) {
423
+ const contentBlocks: Array<Record<string, unknown>> = [
424
+ { type: "input_text", text: "Attached image(s) from tool result:" },
425
+ ];
426
+ for (const block of message.content) {
427
+ if (block.type !== "image") continue;
428
+ contentBlocks.push({
429
+ type: "input_image",
430
+ detail: "auto",
431
+ image_url: `data:${block.mimeType};base64,${block.data}`,
432
+ });
433
+ }
434
+ input.push({ type: "message", role: "user", content: contentBlocks });
435
+ }
436
+ }
437
+
438
+ msgIndex++;
439
+ }
440
+
441
+ return input;
442
+ }
443
+
444
+ // ============================================================================
445
+ // Endpoint requests
446
+ // ============================================================================
447
+
448
+ export async function requestOpenAiRemoteCompaction(
449
+ model: Model,
450
+ apiKey: string,
451
+ compactInput: Array<Record<string, unknown>>,
452
+ instructions: string,
453
+ signal?: AbortSignal,
454
+ ): Promise<OpenAiRemoteCompactionResponse> {
455
+ const endpoint = resolveOpenAiCompactEndpoint(model);
456
+ const request: OpenAiRemoteCompactionRequest = {
457
+ model: model.id,
458
+ input: trimOpenAiCompactInput(compactInput, model.contextWindow, instructions),
459
+ instructions,
460
+ };
461
+ const headers: Record<string, string> = {
462
+ "content-type": "application/json",
463
+ Authorization: `Bearer ${apiKey}`,
464
+ ...(model.headers ?? {}),
465
+ };
466
+
467
+ // OpenAI code backend endpoints require additional auth headers
468
+ if (model.provider === "openai-codex") {
469
+ const accountId = getCodexAccountId(apiKey);
470
+ if (accountId) {
471
+ headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
472
+ }
473
+ headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
474
+ headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
475
+ }
476
+
477
+ const response = await fetch(endpoint, {
478
+ method: "POST",
479
+ headers,
480
+ body: JSON.stringify(request),
481
+ signal,
482
+ });
483
+
484
+ if (!response.ok) {
485
+ const errorText = await response.text().catch(() => "");
486
+ logger.warn("OpenAI remote compaction failed", {
487
+ endpoint,
488
+ status: response.status,
489
+ statusText: response.statusText,
490
+ errorText,
491
+ });
492
+ throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
493
+ }
494
+
495
+ const data = (await response.json()) as { output?: unknown[] } | undefined;
496
+ const rawOutput = data?.output ?? [];
497
+ const replacementHistory = rawOutput.filter(
498
+ (item): item is Record<string, unknown> =>
499
+ !!item && typeof item === "object" && shouldKeepOpenAiCompactOutputItem(item as Record<string, unknown>),
500
+ );
501
+ const compactionItem = replacementHistory.findLast((item): item is OpenAiRemoteCompactionItem => {
502
+ if (item.type === "compaction" && typeof item.encrypted_content === "string") return true;
503
+ if (item.type === "compaction_summary") return true;
504
+ return false;
505
+ });
506
+ if (!compactionItem) {
507
+ const outputTypes = rawOutput.map(item =>
508
+ typeof item === "object" && item !== null ? (item as Record<string, unknown>).type : typeof item,
509
+ );
510
+ logger.warn("Remote compaction response missing compaction item", {
511
+ endpoint,
512
+ model: model.id,
513
+ provider: model.provider,
514
+ rawOutputLength: rawOutput.length,
515
+ outputTypes,
516
+ replacementHistoryLength: replacementHistory.length,
517
+ });
518
+ throw new Error("Remote compaction response missing compaction item");
519
+ }
520
+ return { provider: model.provider, replacementHistory, compactionItem };
521
+ }
522
+
523
+ export async function requestRemoteCompaction(
524
+ endpoint: string,
525
+ request: RemoteCompactionRequest,
526
+ signal?: AbortSignal,
527
+ ): Promise<RemoteCompactionResponse> {
528
+ const response = await fetch(endpoint, {
529
+ method: "POST",
530
+ headers: { "content-type": "application/json" },
531
+ body: JSON.stringify(request),
532
+ signal,
533
+ });
534
+
535
+ if (!response.ok) {
536
+ const errorText = await response.text().catch(() => "");
537
+ logger.warn("Remote compaction failed", {
538
+ endpoint,
539
+ status: response.status,
540
+ statusText: response.statusText,
541
+ errorText,
542
+ });
543
+ throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
544
+ }
545
+
546
+ const data = (await response.json()) as RemoteCompactionResponse | undefined;
547
+ if (!data || typeof data.summary !== "string") {
548
+ throw new Error("Remote compaction response missing summary");
549
+ }
550
+
551
+ return data;
552
+ }
@@ -0,0 +1 @@
1
+ Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.
@@ -0,0 +1,5 @@
1
+ The following is a summary of a branch that this conversation came back from:
2
+
3
+ <summary>
4
+ {{summary}}
5
+ </summary>
@@ -0,0 +1,2 @@
1
+ The user explored a different conversation branch before returning here.
2
+ Summary of that exploration:
@@ -0,0 +1,30 @@
1
+ You MUST create a structured summary of the conversation branch for context when returning.
2
+
3
+ You MUST use EXACT format:
4
+
5
+ ## Goal
6
+
7
+ [What user trying to accomplish in this branch?]
8
+
9
+ ## Constraints & Preferences
10
+ - [Constraints, preferences, requirements mentioned]
11
+ - [(none) if none mentioned]
12
+
13
+ ## Progress
14
+
15
+ ### Done
16
+ - [x] [Completed tasks/changes]
17
+
18
+ ### In Progress
19
+ - [ ] [Work started but not finished]
20
+
21
+ ### Blocked
22
+ - [Issues preventing progress]
23
+
24
+ ## Key Decisions
25
+ - **[Decision]**: [Brief rationale]
26
+
27
+ ## Next Steps
28
+ 1. [What should happen next to continue]
29
+
30
+ Sections MUST be kept concise. You MUST preserve exact file paths, function names, error messages.
@@ -0,0 +1,9 @@
1
+ You MUST summarize what was done in this conversation, written like a pull request description.
2
+
3
+ Rules:
4
+ - MUST be 2-3 sentences max
5
+ - MUST describe the changes made, not the process
6
+ - NEVER mention running tests, builds, or other validation steps
7
+ - NEVER explain what the user asked for
8
+ - MUST write in first person (I added…, I fixed…)
9
+ - NEVER ask questions
@@ -0,0 +1,5 @@
1
+ Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. You MUST use this to build on the work that has already been done and NEVER duplicate work. Here is the summary produced by the other language model; you MUST use the information in this summary to assist with your own analysis:
2
+
3
+ <summary>
4
+ {{summary}}
5
+ </summary>
@@ -0,0 +1,38 @@
1
+ You MUST summarize the conversation above into a structured context checkpoint handoff summary for another LLM to resume task.
2
+
3
+ IMPORTANT: If conversation ends with unanswered question to user or imperative/request awaiting user response (e.g., "Please run command and paste output"), you MUST preserve that exact question/request.
4
+
5
+ You MUST use this format (sections can be omitted if not applicable):
6
+
7
+ ## Goal
8
+ [User goals; list multiple if session covers different tasks.]
9
+
10
+ ## Constraints & Preferences
11
+ - [Constraints or requirements mentioned]
12
+
13
+ ## Progress
14
+
15
+ ### Done
16
+ - [x] [Completed tasks/changes]
17
+
18
+ ### In Progress
19
+ - [ ] [Current work]
20
+
21
+ ### Blocked
22
+ - [Issues preventing progress]
23
+
24
+ ## Key Decisions
25
+ - **[Decision]**: [Brief rationale]
26
+
27
+ ## Next Steps
28
+ 1. [Ordered list of next actions]
29
+
30
+ ## Critical Context
31
+ - [Important data, pending questions, references]
32
+
33
+ ## Additional Notes
34
+ [Anything else important not covered above]
35
+
36
+ You MUST output only the structured summary; you NEVER include extra text.
37
+
38
+ Sections MUST be kept concise. You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results. You MUST include repository state changes (branch, uncommitted changes) if mentioned.
@@ -0,0 +1,17 @@
1
+ This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
2
+
3
+ You MUST summarize the prefix to provide context for the retained suffix:
4
+
5
+ ## Original Request
6
+
7
+ [What did the user ask for in this turn?]
8
+
9
+ ## Early Progress
10
+ - [Key decisions and work done in the prefix]
11
+
12
+ ## Context for Suffix
13
+ - [Information needed to understand the retained recent work]
14
+
15
+ You MUST output only the structured summary. You NEVER include extra text.
16
+
17
+ You MUST be concise. You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results if they appear. You MUST focus on what's needed to understand the kept suffix.