@vib-rato/agent-core 0.16.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +852 -0
  2. package/README.md +493 -0
  3. package/dist/types/agent-loop.d.ts +229 -0
  4. package/dist/types/agent.d.ts +533 -0
  5. package/dist/types/append-only-context.d.ts +141 -0
  6. package/dist/types/attempt-scope.d.ts +84 -0
  7. package/dist/types/compaction/adaptive.d.ts +31 -0
  8. package/dist/types/compaction/branch-summarization.d.ts +103 -0
  9. package/dist/types/compaction/compaction.d.ts +330 -0
  10. package/dist/types/compaction/entries.d.ts +124 -0
  11. package/dist/types/compaction/errors.d.ts +26 -0
  12. package/dist/types/compaction/index.d.ts +12 -0
  13. package/dist/types/compaction/messages.d.ts +61 -0
  14. package/dist/types/compaction/openai.d.ts +65 -0
  15. package/dist/types/compaction/pruning.d.ts +130 -0
  16. package/dist/types/compaction/utils.d.ts +32 -0
  17. package/dist/types/compaction.d.ts +1 -0
  18. package/dist/types/harmony-leak.d.ts +100 -0
  19. package/dist/types/heap-eviction-retainers.test.d.ts +1 -0
  20. package/dist/types/image-placeholder-guard.d.ts +4 -0
  21. package/dist/types/index.d.ts +13 -0
  22. package/dist/types/proxy.d.ts +95 -0
  23. package/dist/types/run-collector.d.ts +223 -0
  24. package/dist/types/run-resource-ledger.d.ts +2 -0
  25. package/dist/types/telemetry.d.ts +605 -0
  26. package/dist/types/thinking.d.ts +18 -0
  27. package/dist/types/tool-dispatch-identity.d.ts +27 -0
  28. package/dist/types/types.d.ts +790 -0
  29. package/package.json +72 -0
  30. package/src/agent-loop.ts +5632 -0
  31. package/src/agent.ts +2437 -0
  32. package/src/append-only-context.ts +496 -0
  33. package/src/attempt-scope.ts +195 -0
  34. package/src/compaction/adaptive.ts +92 -0
  35. package/src/compaction/branch-summarization.ts +358 -0
  36. package/src/compaction/compaction.ts +1569 -0
  37. package/src/compaction/entries.ts +158 -0
  38. package/src/compaction/errors.ts +31 -0
  39. package/src/compaction/index.ts +13 -0
  40. package/src/compaction/messages.ts +212 -0
  41. package/src/compaction/openai.ts +580 -0
  42. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  43. package/src/compaction/prompts/branch-summary-context.md +5 -0
  44. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  45. package/src/compaction/prompts/branch-summary.md +30 -0
  46. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  47. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  48. package/src/compaction/prompts/compaction-summary.md +38 -0
  49. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  50. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  51. package/src/compaction/prompts/file-operations.md +10 -0
  52. package/src/compaction/prompts/handoff-document.md +56 -0
  53. package/src/compaction/prompts/summarization-system.md +3 -0
  54. package/src/compaction/pruning.ts +1026 -0
  55. package/src/compaction/utils.ts +189 -0
  56. package/src/compaction.ts +1 -0
  57. package/src/harmony-leak.ts +457 -0
  58. package/src/heap-eviction-retainers.test.ts +293 -0
  59. package/src/image-placeholder-guard.ts +20 -0
  60. package/src/index.ts +23 -0
  61. package/src/prompts/escaped-nonascii-recovery.md +3 -0
  62. package/src/prompts/repeated-tool-failure-recovery.md +1 -0
  63. package/src/proxy.ts +408 -0
  64. package/src/run-collector.ts +728 -0
  65. package/src/run-resource-ledger.ts +345 -0
  66. package/src/telemetry.ts +2161 -0
  67. package/src/thinking.ts +20 -0
  68. package/src/tool-dispatch-identity.ts +87 -0
  69. package/src/types.ts +882 -0
@@ -0,0 +1,580 @@
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 "@vib-rato/ai/providers/openai-codex/constants";
21
+ import { parseTextSignature } from "@vib-rato/ai/providers/openai-responses-shared";
22
+ import { transformMessages } from "@vib-rato/ai/providers/transform-messages";
23
+ import type { AssistantMessage, Message, Model } from "@vib-rato/ai/types";
24
+ import {
25
+ getOpenAIResponsesHistoryItems,
26
+ getOpenAIResponsesHistoryPayload,
27
+ neutralizeReservedControlTokens,
28
+ neutralizeResponsesInputControlTokens,
29
+ normalizeResponsesToolCallId,
30
+ } from "@vib-rato/ai/utils";
31
+ import { $credentialEnv, logger } from "@vib-rato/utils";
32
+
33
+ const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
34
+
35
+ // ============================================================================
36
+ // Public types
37
+ // ============================================================================
38
+
39
+ export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
40
+
41
+ export type OpenAiRemoteCompactionItem = {
42
+ type: "compaction" | "compaction_summary";
43
+ encrypted_content?: string;
44
+ summary?: string;
45
+ };
46
+
47
+ export interface OpenAiRemoteCompactionPreserveData {
48
+ provider?: string;
49
+ replacementHistory: Array<Record<string, unknown>>;
50
+ compactionItem: OpenAiRemoteCompactionItem;
51
+ }
52
+
53
+ export interface OpenAiRemoteCompactionRequest {
54
+ model: string;
55
+ input: Array<Record<string, unknown>>;
56
+ instructions: string;
57
+ }
58
+
59
+ export interface OpenAiRemoteCompactionResponse extends OpenAiRemoteCompactionPreserveData {}
60
+
61
+ export interface RemoteCompactionRequest {
62
+ systemPrompt: string;
63
+ prompt: string;
64
+ }
65
+
66
+ export interface RemoteCompactionResponse {
67
+ summary: string;
68
+ shortSummary?: string;
69
+ }
70
+
71
+ // ============================================================================
72
+ // OpenAI provider gating + endpoint resolution
73
+ // ============================================================================
74
+
75
+ export function shouldUseOpenAiRemoteCompaction(model: Model): boolean {
76
+ return model.provider === "openai" || model.provider === "openai-codex";
77
+ }
78
+
79
+ function resolveOpenAiCompactEndpoint(model: Model, authCredentialType?: "api_key" | "oauth"): string {
80
+ if (model.provider === "openai-codex") {
81
+ return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
82
+ }
83
+
84
+ // Trusted sources only: the compaction endpoint carries the OpenAI credential.
85
+ const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
86
+ const configuredBaseUrl = model.baseUrl?.trim();
87
+ const rawBase =
88
+ authCredentialType === "oauth"
89
+ ? OPENAI_DEFAULT_BASE_URL
90
+ : envBaseUrl && (!configuredBaseUrl || configuredBaseUrl.toLowerCase().includes("api.openai.com"))
91
+ ? envBaseUrl
92
+ : configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
93
+ const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
94
+ if (normalizedBase.endsWith("/v1")) return `${normalizedBase}/responses/compact`;
95
+ return `${normalizedBase}/v1/responses/compact`;
96
+ }
97
+
98
+ /** Test seam: the compaction endpoint as resolved from trusted env. */
99
+ export function resolveOpenAiCompactEndpointForTest(model: Model, authCredentialType?: "api_key" | "oauth"): string {
100
+ return resolveOpenAiCompactEndpoint(model, authCredentialType);
101
+ }
102
+
103
+ function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
104
+ const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
105
+ const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
106
+ if (/\/codex(?:\/v\d+)?$/.test(normalizedBase)) return `${normalizedBase}/responses/compact`;
107
+ return `${normalizedBase}/codex/responses/compact`;
108
+ }
109
+
110
+ function normalizeOpenAiCompactionToolCallId(id: string): string {
111
+ const normalized = normalizeResponsesToolCallId(id);
112
+ return `${normalized.callId}|${normalized.itemId ?? normalized.callId}`;
113
+ }
114
+
115
+ // ============================================================================
116
+ // Preserve-data helpers
117
+ // ============================================================================
118
+
119
+ export function getPreservedOpenAiRemoteCompactionData(
120
+ preserveData: Record<string, unknown> | undefined,
121
+ ): OpenAiRemoteCompactionPreserveData | undefined {
122
+ const candidate = preserveData?.[OPENAI_REMOTE_COMPACTION_PRESERVE_KEY];
123
+ if (!candidate || typeof candidate !== "object") return undefined;
124
+ const maybeData = candidate as { provider?: unknown; replacementHistory?: unknown; compactionItem?: unknown };
125
+ if (!Array.isArray(maybeData.replacementHistory)) return undefined;
126
+ const maybeItem = maybeData.compactionItem;
127
+ if (!maybeItem || typeof maybeItem !== "object") return undefined;
128
+ const compactionItem = maybeItem as { type?: unknown; encrypted_content?: unknown; summary?: unknown };
129
+ const isClassicCompaction =
130
+ compactionItem.type === "compaction" && typeof compactionItem.encrypted_content === "string";
131
+ const isSummaryCompaction = compactionItem.type === "compaction_summary";
132
+ if (!isClassicCompaction && !isSummaryCompaction) {
133
+ return undefined;
134
+ }
135
+ return {
136
+ provider: typeof maybeData.provider === "string" ? maybeData.provider : undefined,
137
+ replacementHistory: maybeData.replacementHistory as Array<Record<string, unknown>>,
138
+ compactionItem: compactionItem as unknown as OpenAiRemoteCompactionItem,
139
+ };
140
+ }
141
+
142
+ export function withOpenAiRemoteCompactionPreserveData(
143
+ preserveData: Record<string, unknown> | undefined,
144
+ remoteCompaction: OpenAiRemoteCompactionPreserveData | undefined,
145
+ ): Record<string, unknown> | undefined {
146
+ if (remoteCompaction) {
147
+ return {
148
+ ...(preserveData ?? {}),
149
+ [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: remoteCompaction,
150
+ };
151
+ }
152
+
153
+ if (!preserveData || !(OPENAI_REMOTE_COMPACTION_PRESERVE_KEY in preserveData)) {
154
+ return preserveData;
155
+ }
156
+
157
+ const { [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: _removed, ...rest } = preserveData;
158
+ return Object.keys(rest).length > 0 ? rest : undefined;
159
+ }
160
+
161
+ // ============================================================================
162
+ // Input/output filtering for OpenAI compact endpoint
163
+ // ============================================================================
164
+
165
+ export function estimateOpenAiCompactInputTokens(input: Array<Record<string, unknown>>, instructions: string): number {
166
+ let chars = instructions.length;
167
+ for (const item of input) {
168
+ chars += JSON.stringify(item).length;
169
+ }
170
+ return Math.ceil(chars / 4);
171
+ }
172
+
173
+ function shouldTrimOpenAiCompactInputItem(item: Record<string, unknown>): boolean {
174
+ return item.type === "function_call_output" || (item.type === "message" && item.role === "developer");
175
+ }
176
+
177
+ function shouldKeepOpenAiCompactOutputUserMessage(item: Record<string, unknown>): boolean {
178
+ if (item.role !== "user") return false;
179
+ const content = item.content;
180
+ if (!Array.isArray(content) || content.length === 0) return false;
181
+ const contextualFragmentPatterns = [
182
+ [/^<system-reminder>[\s\S]*<\/system-reminder>$/i, /<system-reminder>/i],
183
+ [/^#\s*AGENTS\.md instructions for\b[\s\S]*<\/INSTRUCTIONS>$/i, /# AGENTS.md instructions/],
184
+ [/^<environment-context>[\s\S]*<\/environment-context>$/i, /<environment-context>/i],
185
+ [/^<skill>[\s\S]*<\/skill>$/i, /<skill>/i],
186
+ [/^<user-shell-command>[\s\S]*<\/user-shell-command>$/i, /<user-shell-command>/i],
187
+ [/^<turn-aborted>[\s\S]*<\/turn-aborted>$/i, /<turn-aborted>/i],
188
+ [/^<subagent-notification>[\s\S]*<\/subagent-notification>$/i, /<subagent-notification>/i],
189
+ ] as const;
190
+ return content.every(part => {
191
+ if (!part || typeof part !== "object") return false;
192
+ const candidate = part as { type?: unknown; text?: unknown };
193
+ if (candidate.type === "input_image") return true;
194
+ if (candidate.type !== "input_text" || typeof candidate.text !== "string") return false;
195
+ const trimmed = candidate.text.trim();
196
+ if (trimmed.length === 0) return false;
197
+ return !contextualFragmentPatterns.some(([strictPattern, markerPattern]) => {
198
+ return strictPattern.test(trimmed) || markerPattern.test(trimmed);
199
+ });
200
+ });
201
+ }
202
+
203
+ function shouldKeepOpenAiCompactOutputItem(item: Record<string, unknown>): boolean {
204
+ if (item.type === "compaction" || item.type === "compaction_summary") return true;
205
+ if (item.type !== "message") return false;
206
+ if (item.role === "developer") return false;
207
+ if (item.role === "assistant") return true;
208
+ return shouldKeepOpenAiCompactOutputUserMessage(item);
209
+ }
210
+
211
+ export function trimOpenAiCompactInput(
212
+ input: Array<Record<string, unknown>>,
213
+ contextWindow: number,
214
+ instructions: string,
215
+ ): Array<Record<string, unknown>> {
216
+ const itemLengths = input.map(item => JSON.stringify(item).length);
217
+ let chars = instructions.length;
218
+ for (const length of itemLengths) chars += length;
219
+
220
+ function removeAt(index: number): void {
221
+ chars -= itemLengths[index] ?? 0;
222
+ trimmed.splice(index, 1);
223
+ itemLengths.splice(index, 1);
224
+ }
225
+ const trimmed = [...input];
226
+ while (trimmed.length > 0 && Math.ceil(chars / 4) > contextWindow) {
227
+ const last = trimmed[trimmed.length - 1];
228
+ if (last?.type === "function_call_output" || last?.type === "custom_tool_call_output") {
229
+ const callId = typeof last.call_id === "string" ? last.call_id : undefined;
230
+ const callType = last.type === "custom_tool_call_output" ? "custom_tool_call" : "function_call";
231
+ removeAt(trimmed.length - 1);
232
+ if (callId) {
233
+ const matchingCallIndex = trimmed.findLastIndex(item => item.type === callType && item.call_id === callId);
234
+ if (matchingCallIndex >= 0) {
235
+ removeAt(matchingCallIndex);
236
+ }
237
+ }
238
+ continue;
239
+ }
240
+ if (!last || !shouldTrimOpenAiCompactInputItem(last)) {
241
+ break;
242
+ }
243
+ removeAt(trimmed.length - 1);
244
+ }
245
+ return trimmed;
246
+ }
247
+
248
+ export function resolveOpenAiCompactInputBudget(contextWindow: number, maxOutputTokens = 0): number {
249
+ if (contextWindow <= 0) return 0;
250
+ const reservedTokens = Math.max(Math.floor(contextWindow * 0.15), maxOutputTokens, 1);
251
+ return Math.max(1, contextWindow - reservedTokens);
252
+ }
253
+
254
+ function collectKnownOpenAiCallIds(items: Array<Record<string, unknown>>): Set<string> {
255
+ const knownCallIds = new Set<string>();
256
+ for (const item of items) {
257
+ if ((item.type === "function_call" || item.type === "custom_tool_call") && typeof item.call_id === "string") {
258
+ knownCallIds.add(item.call_id);
259
+ }
260
+ }
261
+ return knownCallIds;
262
+ }
263
+
264
+ function collectCustomOpenAiCallIds(items: Array<Record<string, unknown>>): Set<string> {
265
+ const customCallIds = new Set<string>();
266
+ for (const item of items) {
267
+ if (item.type === "custom_tool_call" && typeof item.call_id === "string") {
268
+ customCallIds.add(item.call_id);
269
+ }
270
+ }
271
+ return customCallIds;
272
+ }
273
+
274
+ // ============================================================================
275
+ // Native history construction (responses-API shape)
276
+ // ============================================================================
277
+
278
+ /**
279
+ * Build the OpenAI Responses-API native history array from LLM messages.
280
+ *
281
+ * Caller is responsible for converting any custom message types to
282
+ * `Message[]` first (e.g. via the agent's `convertToLlm`); this function
283
+ * operates purely on the LLM-domain shape.
284
+ *
285
+ * @param messages - LLM messages to encode.
286
+ * @param model - Target model (used for provider gating + tool-call id rules).
287
+ * @param previousReplacementHistory - History from a prior compaction whose
288
+ * encrypted reasoning we want to preserve.
289
+ */
290
+ export function buildOpenAiNativeHistory(
291
+ messages: Message[],
292
+ model: Model,
293
+ previousReplacementHistory?: Array<Record<string, unknown>>,
294
+ ): Array<Record<string, unknown>> {
295
+ const input: Array<Record<string, unknown>> = previousReplacementHistory ? [...previousReplacementHistory] : [];
296
+ const transformedMessages = transformMessages(messages, model, id => normalizeOpenAiCompactionToolCallId(id));
297
+
298
+ let msgIndex = 0;
299
+ let knownCallIds = collectKnownOpenAiCallIds(input);
300
+ let customCallIds = collectCustomOpenAiCallIds(input);
301
+ for (const message of transformedMessages) {
302
+ if (message.role === "user" || message.role === "developer") {
303
+ const providerPayload = (message as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
304
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
305
+ if (historyItems) {
306
+ input.push(...historyItems);
307
+ knownCallIds = collectKnownOpenAiCallIds(input);
308
+ customCallIds = collectCustomOpenAiCallIds(input);
309
+ msgIndex++;
310
+ continue;
311
+ }
312
+
313
+ const contentBlocks: Array<Record<string, unknown>> = [];
314
+ if (typeof message.content === "string") {
315
+ if (message.content.trim().length > 0) {
316
+ contentBlocks.push({ type: "input_text", text: message.content.toWellFormed() });
317
+ }
318
+ } else {
319
+ for (const block of message.content) {
320
+ if (block.type === "text") {
321
+ if (!block.text || block.text.trim().length === 0) continue;
322
+ contentBlocks.push({ type: "input_text", text: block.text.toWellFormed() });
323
+ continue;
324
+ }
325
+ if (block.type === "image") {
326
+ contentBlocks.push({
327
+ type: "input_image",
328
+ detail: "auto",
329
+ image_url: `data:${block.mimeType};base64,${block.data}`,
330
+ });
331
+ }
332
+ }
333
+ }
334
+ if (contentBlocks.length > 0) {
335
+ input.push({ type: "message", role: message.role, content: contentBlocks });
336
+ }
337
+ msgIndex++;
338
+ continue;
339
+ }
340
+
341
+ if (message.role === "assistant") {
342
+ const assistant = message as AssistantMessage;
343
+ const providerPayload = getOpenAIResponsesHistoryPayload(
344
+ assistant.providerPayload,
345
+ model.provider,
346
+ assistant.provider,
347
+ );
348
+ if (providerPayload) {
349
+ if (providerPayload.dt) {
350
+ input.push(...providerPayload.items);
351
+ } else {
352
+ input.splice(0, input.length, ...providerPayload.items);
353
+ }
354
+ knownCallIds = collectKnownOpenAiCallIds(input);
355
+ customCallIds = collectCustomOpenAiCallIds(input);
356
+ msgIndex++;
357
+ continue;
358
+ }
359
+ const isDifferentModel =
360
+ assistant.model !== model.id && assistant.provider === model.provider && assistant.api === model.api;
361
+
362
+ for (const block of assistant.content) {
363
+ if (block.type === "thinking" && assistant.stopReason !== "error" && block.thinkingSignature) {
364
+ try {
365
+ const reasoningItem = JSON.parse(block.thinkingSignature) as Record<string, unknown>;
366
+ if (reasoningItem && typeof reasoningItem === "object") {
367
+ input.push(reasoningItem);
368
+ }
369
+ } catch {
370
+ logger.warn("Failed to parse assistant reasoning for remote compaction", {
371
+ model: assistant.model,
372
+ provider: assistant.provider,
373
+ });
374
+ }
375
+ continue;
376
+ }
377
+
378
+ if (block.type === "text") {
379
+ if (!block.text || block.text.trim().length === 0) continue;
380
+ const parsedSignature = parseTextSignature(block.textSignature);
381
+ let msgId = parsedSignature?.id;
382
+ if (!msgId) {
383
+ msgId = `msg_${msgIndex}`;
384
+ } else if (msgId.length > 64) {
385
+ msgId = `msg_${Bun.hash(msgId).toString(36)}`;
386
+ }
387
+ input.push({
388
+ type: "message",
389
+ role: "assistant",
390
+ content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
391
+ status: "completed",
392
+ id: msgId,
393
+ phase: parsedSignature?.phase,
394
+ });
395
+ continue;
396
+ }
397
+
398
+ if (block.type === "toolCall") {
399
+ const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
400
+ let itemId: string | undefined = normalized.itemId;
401
+ if (
402
+ isDifferentModel &&
403
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
404
+ ) {
405
+ itemId = undefined;
406
+ }
407
+ knownCallIds.add(normalized.callId);
408
+ if (block.customWireName) {
409
+ const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
410
+ customCallIds.add(normalized.callId);
411
+ input.push({
412
+ type: "custom_tool_call",
413
+ id: itemId,
414
+ call_id: normalized.callId,
415
+ name: block.customWireName,
416
+ input: rawInput,
417
+ });
418
+ continue;
419
+ }
420
+ input.push({
421
+ type: "function_call",
422
+ id: itemId,
423
+ call_id: normalized.callId,
424
+ name: block.name,
425
+ arguments: JSON.stringify(block.arguments),
426
+ });
427
+ }
428
+ }
429
+
430
+ msgIndex++;
431
+ continue;
432
+ }
433
+
434
+ if (message.role === "toolResult") {
435
+ const normalized = normalizeResponsesToolCallId(message.toolCallId);
436
+ if (!knownCallIds.has(normalized.callId)) {
437
+ msgIndex++;
438
+ continue;
439
+ }
440
+
441
+ const textOutput = message.content
442
+ .filter(block => block.type === "text")
443
+ .map(block => block.text)
444
+ .join("\n");
445
+ const hasImages = message.content.some(block => block.type === "image");
446
+ const outputText = textOutput.length > 0 ? textOutput : hasImages ? "(see attached image)" : "";
447
+ input.push({
448
+ type: customCallIds.has(normalized.callId) ? "custom_tool_call_output" : "function_call_output",
449
+ call_id: normalized.callId,
450
+ output: outputText.toWellFormed(),
451
+ });
452
+
453
+ if (hasImages && model.input.includes("image")) {
454
+ const contentBlocks: Array<Record<string, unknown>> = [
455
+ { type: "input_text", text: "Attached image(s) from tool result:" },
456
+ ];
457
+ for (const block of message.content) {
458
+ if (block.type !== "image") continue;
459
+ contentBlocks.push({
460
+ type: "input_image",
461
+ detail: "auto",
462
+ image_url: `data:${block.mimeType};base64,${block.data}`,
463
+ });
464
+ }
465
+ input.push({ type: "message", role: "user", content: contentBlocks });
466
+ }
467
+ }
468
+
469
+ msgIndex++;
470
+ }
471
+
472
+ return input;
473
+ }
474
+
475
+ // ============================================================================
476
+ // Endpoint requests
477
+ // ============================================================================
478
+
479
+ export async function requestOpenAiRemoteCompaction(
480
+ model: Model,
481
+ apiKey: string,
482
+ compactInput: Array<Record<string, unknown>>,
483
+ instructions: string,
484
+ signal?: AbortSignal,
485
+ options?: { authCredentialType?: "api_key" | "oauth" },
486
+ ): Promise<OpenAiRemoteCompactionResponse> {
487
+ const endpoint = resolveOpenAiCompactEndpoint(model, options?.authCredentialType);
488
+ const request: OpenAiRemoteCompactionRequest = {
489
+ model: model.id,
490
+ input: neutralizeResponsesInputControlTokens(
491
+ trimOpenAiCompactInput(
492
+ compactInput,
493
+ resolveOpenAiCompactInputBudget(model.contextWindow, model.maxTokens),
494
+ instructions,
495
+ ),
496
+ ),
497
+ instructions,
498
+ };
499
+ const headers: Record<string, string> = {
500
+ "content-type": "application/json",
501
+ Authorization: `Bearer ${apiKey}`,
502
+ ...(model.headers ?? {}),
503
+ };
504
+
505
+ // OpenAI code backend endpoints require additional auth headers
506
+ if (model.provider === "openai-codex") {
507
+ const accountId = getCodexAccountId(apiKey);
508
+ if (accountId) {
509
+ headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
510
+ }
511
+ headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
512
+ headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
513
+ }
514
+
515
+ const response = await fetch(endpoint, {
516
+ method: "POST",
517
+ headers,
518
+ body: JSON.stringify(request),
519
+ signal,
520
+ });
521
+
522
+ if (!response.ok) {
523
+ throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
524
+ }
525
+
526
+ const data = (await response.json()) as { output?: unknown } | undefined;
527
+ if (!Array.isArray(data?.output)) {
528
+ throw new Error(`Remote compaction response malformed output (outputType=${typeof data?.output})`);
529
+ }
530
+ const rawOutput = data.output;
531
+ const replacementHistory = rawOutput.filter(
532
+ (item): item is Record<string, unknown> =>
533
+ !!item && typeof item === "object" && shouldKeepOpenAiCompactOutputItem(item as Record<string, unknown>),
534
+ );
535
+ const compactionItem = replacementHistory.findLast((item): item is OpenAiRemoteCompactionItem => {
536
+ if (item.type === "compaction" && typeof item.encrypted_content === "string") return true;
537
+ if (item.type === "compaction_summary") return true;
538
+ return false;
539
+ });
540
+ if (!compactionItem) {
541
+ const outputTypes = rawOutput.map(item =>
542
+ typeof item === "object" && item !== null ? (item as Record<string, unknown>).type : typeof item,
543
+ );
544
+ throw new Error(
545
+ `Remote compaction response missing compaction item (rawOutputLength=${rawOutput.length}, outputTypes=${outputTypes.join(",")}, replacementHistoryLength=${replacementHistory.length})`,
546
+ );
547
+ }
548
+ return { provider: model.provider, replacementHistory, compactionItem };
549
+ }
550
+
551
+ export async function requestRemoteCompaction(
552
+ endpoint: string,
553
+ request: RemoteCompactionRequest,
554
+ signal?: AbortSignal,
555
+ ): Promise<RemoteCompactionResponse> {
556
+ // The prompt embeds the serialized transcript, which can carry leaked Harmony
557
+ // control-token markers (e.g. `<|channel|>analysis`) from model output; a
558
+ // gpt-5.6-backed summarization endpoint rejects those with `Request blocked`.
559
+ const sanitizedRequest: RemoteCompactionRequest = {
560
+ systemPrompt: neutralizeReservedControlTokens(request.systemPrompt),
561
+ prompt: neutralizeReservedControlTokens(request.prompt),
562
+ };
563
+ const response = await fetch(endpoint, {
564
+ method: "POST",
565
+ headers: { "content-type": "application/json" },
566
+ body: JSON.stringify(sanitizedRequest),
567
+ signal,
568
+ });
569
+
570
+ if (!response.ok) {
571
+ throw new Error(`Remote compaction failed (${response.status} ${response.statusText})`);
572
+ }
573
+
574
+ const data = (await response.json()) as RemoteCompactionResponse | undefined;
575
+ if (!data || typeof data.summary !== "string") {
576
+ throw new Error("Remote compaction response missing summary");
577
+ }
578
+
579
+ return data;
580
+ }
@@ -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.