@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.
- package/CHANGELOG.md +482 -0
- package/README.md +473 -0
- package/dist/types/agent-loop.d.ts +55 -0
- package/dist/types/agent.d.ts +334 -0
- package/dist/types/append-only-context.d.ts +113 -0
- package/dist/types/compaction/branch-summarization.d.ts +94 -0
- package/dist/types/compaction/compaction.d.ts +166 -0
- package/dist/types/compaction/entries.d.ts +103 -0
- package/dist/types/compaction/errors.d.ts +26 -0
- package/dist/types/compaction/index.d.ts +11 -0
- package/dist/types/compaction/messages.d.ts +61 -0
- package/dist/types/compaction/openai.d.ts +58 -0
- package/dist/types/compaction/pruning.d.ts +18 -0
- package/dist/types/compaction/utils.d.ts +32 -0
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/harmony-leak.d.ts +99 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/proxy.d.ts +84 -0
- package/dist/types/run-collector.d.ts +196 -0
- package/dist/types/telemetry.d.ts +588 -0
- package/dist/types/thinking.d.ts +17 -0
- package/dist/types/types.d.ts +407 -0
- package/package.json +75 -0
- package/src/agent-loop.ts +1279 -0
- package/src/agent.ts +1399 -0
- package/src/append-only-context.ts +297 -0
- package/src/compaction/branch-summarization.ts +339 -0
- package/src/compaction/compaction.ts +1065 -0
- package/src/compaction/entries.ts +133 -0
- package/src/compaction/errors.ts +31 -0
- package/src/compaction/index.ts +12 -0
- package/src/compaction/messages.ts +212 -0
- package/src/compaction/openai.ts +552 -0
- package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
- package/src/compaction/prompts/branch-summary-context.md +5 -0
- package/src/compaction/prompts/branch-summary-preamble.md +2 -0
- package/src/compaction/prompts/branch-summary.md +30 -0
- package/src/compaction/prompts/compaction-short-summary.md +9 -0
- package/src/compaction/prompts/compaction-summary-context.md +5 -0
- package/src/compaction/prompts/compaction-summary.md +38 -0
- package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
- package/src/compaction/prompts/compaction-update-summary.md +45 -0
- package/src/compaction/prompts/file-operations.md +10 -0
- package/src/compaction/prompts/handoff-document.md +49 -0
- package/src/compaction/prompts/summarization-system.md +3 -0
- package/src/compaction/pruning.ts +92 -0
- package/src/compaction/utils.ts +185 -0
- package/src/compaction.ts +1 -0
- package/src/harmony-leak.ts +427 -0
- package/src/index.ts +19 -0
- package/src/proxy.ts +326 -0
- package/src/run-collector.ts +631 -0
- package/src/telemetry.ts +2018 -0
- package/src/thinking.ts +19 -0
- 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,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.
|