jeopi-agent-core 16.2.13
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 +1016 -0
- package/README.md +473 -0
- package/dist/types/agent-loop.d.ts +66 -0
- package/dist/types/agent.d.ts +427 -0
- package/dist/types/append-only-context.d.ts +133 -0
- package/dist/types/compaction/branch-summarization.d.ts +101 -0
- package/dist/types/compaction/compaction-v2-streaming.d.ts +82 -0
- package/dist/types/compaction/compaction.d.ts +283 -0
- package/dist/types/compaction/entries.d.ts +110 -0
- package/dist/types/compaction/errors.d.ts +26 -0
- package/dist/types/compaction/index.d.ts +12 -0
- package/dist/types/compaction/messages.d.ts +77 -0
- package/dist/types/compaction/openai.d.ts +77 -0
- package/dist/types/compaction/pruning.d.ts +105 -0
- package/dist/types/compaction/shake.d.ts +92 -0
- package/dist/types/compaction/tool-protection.d.ts +17 -0
- package/dist/types/compaction/utils.d.ts +58 -0
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/proxy.d.ts +85 -0
- package/dist/types/replay-policy.d.ts +5 -0
- package/dist/types/run-collector.d.ts +196 -0
- package/dist/types/telemetry.d.ts +590 -0
- package/dist/types/thinking.d.ts +17 -0
- package/dist/types/tokenizer.d.ts +1 -0
- package/dist/types/types.d.ts +640 -0
- package/dist/types/utils/yield.d.ts +71 -0
- package/package.json +78 -0
- package/src/agent-loop.ts +2188 -0
- package/src/agent.ts +1457 -0
- package/src/append-only-context.ts +348 -0
- package/src/compaction/branch-summarization.ts +370 -0
- package/src/compaction/compaction-v2-streaming.ts +719 -0
- package/src/compaction/compaction.ts +1553 -0
- package/src/compaction/entries.ts +142 -0
- package/src/compaction/errors.ts +31 -0
- package/src/compaction/index.ts +13 -0
- package/src/compaction/messages.ts +237 -0
- package/src/compaction/openai.ts +581 -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 +5 -0
- package/src/compaction/prompts/handoff-document.md +49 -0
- package/src/compaction/prompts/snapcompact-archive-context.md +3 -0
- package/src/compaction/prompts/summarization-system.md +3 -0
- package/src/compaction/pruning.ts +424 -0
- package/src/compaction/shake.ts +429 -0
- package/src/compaction/tool-protection.ts +55 -0
- package/src/compaction/utils.ts +323 -0
- package/src/compaction.ts +1 -0
- package/src/index.ts +24 -0
- package/src/proxy.ts +376 -0
- package/src/replay-policy.ts +13 -0
- package/src/run-collector.ts +631 -0
- package/src/telemetry.ts +2034 -0
- package/src/thinking.ts +19 -0
- package/src/tokenizer.ts +17 -0
- package/src/types.ts +718 -0
- package/src/utils/yield.ts +183 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote compaction utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provider-side conversation summarization endpoints. Three flavors:
|
|
5
|
+
*
|
|
6
|
+
* - **OpenAI remote compaction V2** (Responses streaming): appends a
|
|
7
|
+
* `compaction_trigger` input item to the normal stream and stores the returned
|
|
8
|
+
* `compaction` item with retained real user messages in `preserveData`.
|
|
9
|
+
* - **OpenAI remote compaction V1** (`/responses/compact`): preserves encrypted
|
|
10
|
+
* reasoning across compactions by submitting the full responses-API native
|
|
11
|
+
* history and storing the returned `compaction` / `compaction_summary`
|
|
12
|
+
* item in `preserveData` so future turns can replay the encrypted state.
|
|
13
|
+
* - **Generic remote compaction**: a thin POST helper for self-hosted
|
|
14
|
+
* summarization endpoints that accept `{ systemPrompt, prompt }` and reply
|
|
15
|
+
* with `{ summary, shortSummary? }`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ProviderHttpError } from "jeopi-ai/error";
|
|
19
|
+
import { parseAzureDeploymentNameMap, parseTextSignature } from "jeopi-ai/providers/openai-shared";
|
|
20
|
+
import { transformMessages } from "jeopi-ai/providers/transform-messages";
|
|
21
|
+
import type { Api, AssistantMessage, FetchImpl, Message, Model } from "jeopi-ai/types";
|
|
22
|
+
import {
|
|
23
|
+
getOpenAIResponsesHistoryItems,
|
|
24
|
+
getOpenAIResponsesHistoryPayload,
|
|
25
|
+
normalizeResponsesToolCallId,
|
|
26
|
+
} from "jeopi-ai/utils";
|
|
27
|
+
import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "jeopi-catalog/wire/codex";
|
|
28
|
+
import { $env, logger } from "jeopi-utils";
|
|
29
|
+
|
|
30
|
+
export * from "./compaction-v2-streaming";
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Public types
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hard ceiling on remote compaction HTTP requests. Unlike every provider
|
|
40
|
+
* stream (guarded by first-event/idle watchdogs in pi-ai), these are raw
|
|
41
|
+
* fetches awaiting one non-streamed JSON body — a connection silently dropped
|
|
42
|
+
* by a middlebox would otherwise hang the whole compaction pipeline forever
|
|
43
|
+
* (frozen "Auto context-full maintenance…" spinner, manual /compact queueing
|
|
44
|
+
* behind it). On timeout the caller falls back to local summarization.
|
|
45
|
+
*/
|
|
46
|
+
export const REMOTE_COMPACTION_TIMEOUT_MS = 180_000;
|
|
47
|
+
|
|
48
|
+
const DEFAULT_AZURE_API_VERSION = "v1";
|
|
49
|
+
|
|
50
|
+
/** Race the caller's signal against the request timeout; `timeoutMs <= 0` disables the watchdog. */
|
|
51
|
+
function withRequestTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal | undefined {
|
|
52
|
+
if (timeoutMs <= 0) return signal;
|
|
53
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
54
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type OpenAiRemoteCompactionItem = {
|
|
58
|
+
type: "compaction" | "compaction_summary";
|
|
59
|
+
encrypted_content?: string;
|
|
60
|
+
summary?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface OpenAiRemoteCompactionPreserveData {
|
|
64
|
+
provider?: string;
|
|
65
|
+
replacementHistory: Array<Record<string, unknown>>;
|
|
66
|
+
compactionItem: OpenAiRemoteCompactionItem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OpenAiRemoteCompactionRequest {
|
|
70
|
+
model: string;
|
|
71
|
+
input: Array<Record<string, unknown>>;
|
|
72
|
+
instructions: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface OpenAiRemoteCompactionResponse extends OpenAiRemoteCompactionPreserveData {}
|
|
76
|
+
|
|
77
|
+
export interface RemoteCompactionRequest {
|
|
78
|
+
systemPrompt: string;
|
|
79
|
+
prompt: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RemoteCompactionResponse {
|
|
83
|
+
summary: string;
|
|
84
|
+
shortSummary?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// OpenAI provider gating + endpoint resolution
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
function isOpenAiRemoteCompactionApi(api: Api | undefined): boolean {
|
|
92
|
+
return api === "openai-responses" || api === "azure-openai-responses" || api === "openai-codex-responses";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function shouldUseOpenAiRemoteCompaction(model: Model): boolean {
|
|
96
|
+
if (model.remoteCompaction?.enabled === false) return false;
|
|
97
|
+
if (model.provider === "openai" || model.provider === "openai-codex") return true;
|
|
98
|
+
if (model.remoteCompaction?.enabled !== true) return false;
|
|
99
|
+
return isOpenAiRemoteCompactionApi(model.remoteCompaction.api ?? model.api);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveOpenAiCompactEndpoint(model: Model): string {
|
|
103
|
+
const configuredEndpoint = model.remoteCompaction?.endpoint;
|
|
104
|
+
const compactionApi = model.remoteCompaction?.api ?? model.api;
|
|
105
|
+
if (compactionApi === "azure-openai-responses") {
|
|
106
|
+
return resolveAzureOpenAiCompactEndpoint(model, configuredEndpoint);
|
|
107
|
+
}
|
|
108
|
+
if (configuredEndpoint && configuredEndpoint.length > 0) return configuredEndpoint;
|
|
109
|
+
if (model.provider === "openai-codex" || compactionApi === "openai-codex-responses") {
|
|
110
|
+
return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const defaultBase = "https://api.openai.com/v1";
|
|
114
|
+
const rawBase = model.baseUrl && model.baseUrl.length > 0 ? model.baseUrl : defaultBase;
|
|
115
|
+
const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
|
|
116
|
+
if (normalizedBase.endsWith("/v1")) return `${normalizedBase}/responses/compact`;
|
|
117
|
+
return `${normalizedBase}/v1/responses/compact`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveAzureOpenAiCompactEndpoint(model: Model, configuredEndpoint: string | undefined): string {
|
|
121
|
+
const endpoint =
|
|
122
|
+
configuredEndpoint && configuredEndpoint.length > 0
|
|
123
|
+
? configuredEndpoint
|
|
124
|
+
: `${resolveAzureOpenAiBaseUrl(model)}/responses/compact`;
|
|
125
|
+
return appendAzureApiVersion(endpoint);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveAzureOpenAiBaseUrl(model: Model): string {
|
|
129
|
+
const baseUrl = $env.AZURE_OPENAI_BASE_URL?.trim() || undefined;
|
|
130
|
+
const resourceName = $env.AZURE_OPENAI_RESOURCE_NAME;
|
|
131
|
+
const resolvedBaseUrl =
|
|
132
|
+
baseUrl ?? (resourceName ? `https://${resourceName}.openai.azure.com/openai/v1` : undefined) ?? model.baseUrl;
|
|
133
|
+
if (!resolvedBaseUrl) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or configure model.baseUrl.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return resolvedBaseUrl.replace(/\/+$/, "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function appendAzureApiVersion(endpoint: string): string {
|
|
142
|
+
if (/[?&]api-version=/.test(endpoint)) return endpoint;
|
|
143
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
144
|
+
return `${endpoint}${separator}api-version=${encodeURIComponent($env.AZURE_OPENAI_API_VERSION || DEFAULT_AZURE_API_VERSION)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveOpenAiCompactModel(model: Model): string {
|
|
148
|
+
const requestModel = model.remoteCompaction?.model ?? model.requestModelId ?? model.id;
|
|
149
|
+
const compactionApi = model.remoteCompaction?.api ?? model.api;
|
|
150
|
+
if (compactionApi !== "azure-openai-responses") return requestModel;
|
|
151
|
+
const mappedDeployment = parseAzureDeploymentNameMap($env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).get(requestModel);
|
|
152
|
+
return mappedDeployment ?? requestModel;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
|
|
156
|
+
const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
|
|
157
|
+
const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
|
|
158
|
+
if (/\/codex(?:\/v\d+)?$/.test(normalizedBase)) return `${normalizedBase}/responses/compact`;
|
|
159
|
+
return `${normalizedBase}/codex/responses/compact`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeOpenAiCompactionToolCallId(id: string): string {
|
|
163
|
+
const normalized = normalizeResponsesToolCallId(id);
|
|
164
|
+
return `${normalized.callId}|${normalized.itemId ?? normalized.callId}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Preserve-data helpers
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
export function getPreservedOpenAiRemoteCompactionData(
|
|
172
|
+
preserveData: Record<string, unknown> | undefined,
|
|
173
|
+
): OpenAiRemoteCompactionPreserveData | undefined {
|
|
174
|
+
const candidate = preserveData?.[OPENAI_REMOTE_COMPACTION_PRESERVE_KEY];
|
|
175
|
+
if (!candidate || typeof candidate !== "object") return undefined;
|
|
176
|
+
const maybeData = candidate as { provider?: unknown; replacementHistory?: unknown; compactionItem?: unknown };
|
|
177
|
+
if (!Array.isArray(maybeData.replacementHistory)) return undefined;
|
|
178
|
+
const maybeItem = maybeData.compactionItem;
|
|
179
|
+
if (!maybeItem || typeof maybeItem !== "object") return undefined;
|
|
180
|
+
const compactionItem = maybeItem as { type?: unknown; encrypted_content?: unknown; summary?: unknown };
|
|
181
|
+
const isClassicCompaction =
|
|
182
|
+
compactionItem.type === "compaction" && typeof compactionItem.encrypted_content === "string";
|
|
183
|
+
const isSummaryCompaction = compactionItem.type === "compaction_summary";
|
|
184
|
+
if (!isClassicCompaction && !isSummaryCompaction) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
provider: typeof maybeData.provider === "string" ? maybeData.provider : undefined,
|
|
189
|
+
replacementHistory: maybeData.replacementHistory as Array<Record<string, unknown>>,
|
|
190
|
+
compactionItem: compactionItem as unknown as OpenAiRemoteCompactionItem,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function withOpenAiRemoteCompactionPreserveData(
|
|
195
|
+
preserveData: Record<string, unknown> | undefined,
|
|
196
|
+
remoteCompaction: OpenAiRemoteCompactionPreserveData | undefined,
|
|
197
|
+
): Record<string, unknown> | undefined {
|
|
198
|
+
if (remoteCompaction) {
|
|
199
|
+
return {
|
|
200
|
+
...(preserveData ?? {}),
|
|
201
|
+
[OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: remoteCompaction,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!preserveData || !(OPENAI_REMOTE_COMPACTION_PRESERVE_KEY in preserveData)) {
|
|
206
|
+
return preserveData;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: _removed, ...rest } = preserveData;
|
|
210
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Input/output filtering for OpenAI compact endpoint
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
function shouldKeepOpenAiCompactOutputItem(item: Record<string, unknown>): boolean {
|
|
218
|
+
if (item.type === "compaction" || item.type === "compaction_summary") return true;
|
|
219
|
+
if (item.type !== "message") return false;
|
|
220
|
+
return item.role === "assistant" || item.role === "user";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Register every tool-call id in `items` (and the subset using the custom-tool
|
|
224
|
+
// wire shape) into the running sets. The history builder maintains both sets
|
|
225
|
+
// incrementally as native history is appended, so this only scans the
|
|
226
|
+
// newly-added items (or, after a full-snapshot replace, the fresh input) rather
|
|
227
|
+
// than re-scanning the whole growing history per message — the latter was
|
|
228
|
+
// O(N²) and blocked the event loop for seconds while compacting large codex
|
|
229
|
+
// contexts (frozen spinner until the next forced render).
|
|
230
|
+
function addOpenAiCallIds(
|
|
231
|
+
items: Array<Record<string, unknown>>,
|
|
232
|
+
knownCallIds: Set<string>,
|
|
233
|
+
customCallIds: Set<string>,
|
|
234
|
+
): void {
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
if (typeof item.call_id !== "string") continue;
|
|
237
|
+
if (item.type === "function_call") {
|
|
238
|
+
knownCallIds.add(item.call_id);
|
|
239
|
+
} else if (item.type === "custom_tool_call") {
|
|
240
|
+
knownCallIds.add(item.call_id);
|
|
241
|
+
customCallIds.add(item.call_id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Native history construction (responses-API shape)
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Build the OpenAI Responses-API native history array from LLM messages.
|
|
252
|
+
*
|
|
253
|
+
* Caller is responsible for converting any custom message types to
|
|
254
|
+
* `Message[]` first (e.g. via the agent's `convertToLlm`); this function
|
|
255
|
+
* operates purely on the LLM-domain shape.
|
|
256
|
+
*
|
|
257
|
+
* @param messages - LLM messages to encode.
|
|
258
|
+
* @param model - Target model (used for provider gating + tool-call id rules).
|
|
259
|
+
* @param previousReplacementHistory - History from a prior compaction whose
|
|
260
|
+
* encrypted reasoning we want to preserve.
|
|
261
|
+
*/
|
|
262
|
+
export function buildOpenAiNativeHistory(
|
|
263
|
+
messages: Message[],
|
|
264
|
+
model: Model,
|
|
265
|
+
previousReplacementHistory?: Array<Record<string, unknown>>,
|
|
266
|
+
): Array<Record<string, unknown>> {
|
|
267
|
+
const input: Array<Record<string, unknown>> = previousReplacementHistory ? [...previousReplacementHistory] : [];
|
|
268
|
+
const transformedMessages = transformMessages(messages, model, id => normalizeOpenAiCompactionToolCallId(id));
|
|
269
|
+
|
|
270
|
+
let msgIndex = 0;
|
|
271
|
+
const knownCallIds = new Set<string>();
|
|
272
|
+
const customCallIds = new Set<string>();
|
|
273
|
+
addOpenAiCallIds(input, knownCallIds, customCallIds);
|
|
274
|
+
for (const message of transformedMessages) {
|
|
275
|
+
if (message.role === "user" || message.role === "developer") {
|
|
276
|
+
const providerPayload = (message as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
277
|
+
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
278
|
+
if (historyItems) {
|
|
279
|
+
input.push(...historyItems);
|
|
280
|
+
addOpenAiCallIds(historyItems, knownCallIds, customCallIds);
|
|
281
|
+
msgIndex++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const contentBlocks: Array<Record<string, unknown>> = [];
|
|
286
|
+
if (typeof message.content === "string") {
|
|
287
|
+
if (message.content.trim().length > 0) {
|
|
288
|
+
contentBlocks.push({ type: "input_text", text: message.content.toWellFormed() });
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
for (const block of message.content) {
|
|
292
|
+
if (block.type === "text") {
|
|
293
|
+
if (!block.text || block.text.trim().length === 0) continue;
|
|
294
|
+
contentBlocks.push({ type: "input_text", text: block.text.toWellFormed() });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (block.type === "image") {
|
|
298
|
+
contentBlocks.push({
|
|
299
|
+
type: "input_image",
|
|
300
|
+
detail: "auto",
|
|
301
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (contentBlocks.length > 0) {
|
|
307
|
+
input.push({ type: "message", role: message.role, content: contentBlocks });
|
|
308
|
+
}
|
|
309
|
+
msgIndex++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (message.role === "assistant") {
|
|
314
|
+
const assistant = message as AssistantMessage;
|
|
315
|
+
const providerPayload = getOpenAIResponsesHistoryPayload(
|
|
316
|
+
assistant.providerPayload,
|
|
317
|
+
model.provider,
|
|
318
|
+
assistant.provider,
|
|
319
|
+
);
|
|
320
|
+
if (providerPayload) {
|
|
321
|
+
if (providerPayload.dt) {
|
|
322
|
+
input.push(...providerPayload.items);
|
|
323
|
+
addOpenAiCallIds(providerPayload.items, knownCallIds, customCallIds);
|
|
324
|
+
} else {
|
|
325
|
+
input.splice(0, input.length, ...providerPayload.items);
|
|
326
|
+
knownCallIds.clear();
|
|
327
|
+
customCallIds.clear();
|
|
328
|
+
addOpenAiCallIds(input, knownCallIds, customCallIds);
|
|
329
|
+
}
|
|
330
|
+
msgIndex++;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const isDifferentModel =
|
|
334
|
+
assistant.model !== model.id && assistant.provider === model.provider && assistant.api === model.api;
|
|
335
|
+
|
|
336
|
+
for (const block of assistant.content) {
|
|
337
|
+
if (block.type === "thinking" && assistant.stopReason !== "error" && block.thinkingSignature) {
|
|
338
|
+
try {
|
|
339
|
+
const reasoningItem = JSON.parse(block.thinkingSignature) as Record<string, unknown>;
|
|
340
|
+
if (reasoningItem && typeof reasoningItem === "object") {
|
|
341
|
+
input.push(reasoningItem);
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
logger.warn("Failed to parse assistant reasoning for remote compaction", {
|
|
345
|
+
model: assistant.model,
|
|
346
|
+
provider: assistant.provider,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (block.type === "text") {
|
|
353
|
+
if (!block.text || block.text.trim().length === 0) continue;
|
|
354
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
355
|
+
let msgId = parsedSignature?.id;
|
|
356
|
+
if (!msgId) {
|
|
357
|
+
msgId = `msg_${msgIndex}`;
|
|
358
|
+
} else if (msgId.length > 64) {
|
|
359
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
360
|
+
}
|
|
361
|
+
input.push({
|
|
362
|
+
type: "message",
|
|
363
|
+
role: "assistant",
|
|
364
|
+
content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
|
|
365
|
+
status: "completed",
|
|
366
|
+
id: msgId,
|
|
367
|
+
phase: parsedSignature?.phase,
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (block.type === "toolCall") {
|
|
373
|
+
const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
|
|
374
|
+
let itemId: string | undefined = normalized.itemId;
|
|
375
|
+
if (
|
|
376
|
+
isDifferentModel &&
|
|
377
|
+
(itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
|
|
378
|
+
) {
|
|
379
|
+
itemId = undefined;
|
|
380
|
+
}
|
|
381
|
+
knownCallIds.add(normalized.callId);
|
|
382
|
+
if (block.customWireName) {
|
|
383
|
+
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
|
|
384
|
+
customCallIds.add(normalized.callId);
|
|
385
|
+
input.push({
|
|
386
|
+
type: "custom_tool_call",
|
|
387
|
+
id: itemId,
|
|
388
|
+
call_id: normalized.callId,
|
|
389
|
+
name: block.customWireName,
|
|
390
|
+
input: rawInput,
|
|
391
|
+
});
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
input.push({
|
|
395
|
+
type: "function_call",
|
|
396
|
+
id: itemId,
|
|
397
|
+
call_id: normalized.callId,
|
|
398
|
+
name: block.name,
|
|
399
|
+
arguments: JSON.stringify(block.arguments),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
msgIndex++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (message.role === "toolResult") {
|
|
409
|
+
const normalized = normalizeResponsesToolCallId(message.toolCallId);
|
|
410
|
+
if (!knownCallIds.has(normalized.callId)) {
|
|
411
|
+
msgIndex++;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const textOutput = message.content
|
|
416
|
+
.filter(block => block.type === "text")
|
|
417
|
+
.map(block => block.text)
|
|
418
|
+
.join("\n");
|
|
419
|
+
const hasImages = message.content.some(block => block.type === "image");
|
|
420
|
+
const outputText = textOutput.length > 0 ? textOutput : hasImages ? "(see attached image)" : "";
|
|
421
|
+
input.push({
|
|
422
|
+
type: customCallIds.has(normalized.callId) ? "custom_tool_call_output" : "function_call_output",
|
|
423
|
+
call_id: normalized.callId,
|
|
424
|
+
output: outputText.toWellFormed(),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (hasImages && model.input.includes("image")) {
|
|
428
|
+
const contentBlocks: Array<Record<string, unknown>> = [
|
|
429
|
+
{ type: "input_text", text: "Attached image(s) from tool result:" },
|
|
430
|
+
];
|
|
431
|
+
for (const block of message.content) {
|
|
432
|
+
if (block.type !== "image") continue;
|
|
433
|
+
contentBlocks.push({
|
|
434
|
+
type: "input_image",
|
|
435
|
+
detail: "auto",
|
|
436
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
input.push({ type: "message", role: "user", content: contentBlocks });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
msgIndex++;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return input;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// Endpoint requests
|
|
451
|
+
// ============================================================================
|
|
452
|
+
export async function requestOpenAiRemoteCompaction(
|
|
453
|
+
model: Model,
|
|
454
|
+
apiKey: string,
|
|
455
|
+
compactInput: Array<Record<string, unknown>>,
|
|
456
|
+
instructions: string,
|
|
457
|
+
signal?: AbortSignal,
|
|
458
|
+
opts?: { fetch?: FetchImpl; timeoutMs?: number },
|
|
459
|
+
): Promise<OpenAiRemoteCompactionResponse> {
|
|
460
|
+
const endpoint = resolveOpenAiCompactEndpoint(model);
|
|
461
|
+
const requestModel = resolveOpenAiCompactModel(model);
|
|
462
|
+
const request: OpenAiRemoteCompactionRequest = {
|
|
463
|
+
model: requestModel,
|
|
464
|
+
// Send full history to the endpoint - don't trim locally.
|
|
465
|
+
// The provider handles compression via the compaction endpoint.
|
|
466
|
+
// Trimming before sending loses assistant messages and thinking blocks.
|
|
467
|
+
input: compactInput,
|
|
468
|
+
instructions,
|
|
469
|
+
};
|
|
470
|
+
const isAzureOpenAiResponses = (model.remoteCompaction?.api ?? model.api) === "azure-openai-responses";
|
|
471
|
+
const headers: Record<string, string> = isAzureOpenAiResponses
|
|
472
|
+
? {
|
|
473
|
+
"content-type": "application/json",
|
|
474
|
+
"api-key": apiKey,
|
|
475
|
+
...(model.headers ?? {}),
|
|
476
|
+
}
|
|
477
|
+
: {
|
|
478
|
+
"content-type": "application/json",
|
|
479
|
+
Authorization: `Bearer ${apiKey}`,
|
|
480
|
+
...(model.headers ?? {}),
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// Codex endpoints require additional auth headers
|
|
484
|
+
if (model.provider === "openai-codex") {
|
|
485
|
+
const accountId = getCodexAccountId(apiKey);
|
|
486
|
+
if (accountId) {
|
|
487
|
+
headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
|
|
488
|
+
}
|
|
489
|
+
headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
490
|
+
headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers,
|
|
496
|
+
body: JSON.stringify(request),
|
|
497
|
+
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
const errorText = await response.text().catch(() => "");
|
|
502
|
+
logger.warn("OpenAI remote compaction failed", {
|
|
503
|
+
endpoint,
|
|
504
|
+
status: response.status,
|
|
505
|
+
statusText: response.statusText,
|
|
506
|
+
errorText,
|
|
507
|
+
});
|
|
508
|
+
throw new ProviderHttpError(
|
|
509
|
+
`Remote compaction failed (${response.status} ${response.statusText})`,
|
|
510
|
+
response.status,
|
|
511
|
+
{
|
|
512
|
+
headers: response.headers,
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const data = (await response.json()) as { output?: unknown[] } | undefined;
|
|
518
|
+
const rawOutput = data?.output ?? [];
|
|
519
|
+
const replacementHistory = rawOutput.filter(
|
|
520
|
+
(item): item is Record<string, unknown> =>
|
|
521
|
+
!!item && typeof item === "object" && shouldKeepOpenAiCompactOutputItem(item as Record<string, unknown>),
|
|
522
|
+
);
|
|
523
|
+
const compactionItem = replacementHistory.findLast((item): item is OpenAiRemoteCompactionItem => {
|
|
524
|
+
if (item.type === "compaction" && typeof item.encrypted_content === "string") return true;
|
|
525
|
+
if (item.type === "compaction_summary") return true;
|
|
526
|
+
return false;
|
|
527
|
+
});
|
|
528
|
+
if (!compactionItem) {
|
|
529
|
+
const outputTypes = rawOutput.map(item =>
|
|
530
|
+
typeof item === "object" && item !== null ? (item as Record<string, unknown>).type : typeof item,
|
|
531
|
+
);
|
|
532
|
+
logger.warn("Remote compaction response missing compaction item", {
|
|
533
|
+
endpoint,
|
|
534
|
+
model: model.id,
|
|
535
|
+
provider: model.provider,
|
|
536
|
+
rawOutputLength: rawOutput.length,
|
|
537
|
+
outputTypes,
|
|
538
|
+
replacementHistoryLength: replacementHistory.length,
|
|
539
|
+
});
|
|
540
|
+
throw new Error("Remote compaction response missing compaction item");
|
|
541
|
+
}
|
|
542
|
+
return { provider: model.provider, replacementHistory, compactionItem };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export async function requestRemoteCompaction(
|
|
546
|
+
endpoint: string,
|
|
547
|
+
request: RemoteCompactionRequest,
|
|
548
|
+
signal?: AbortSignal,
|
|
549
|
+
opts?: { fetch?: FetchImpl; timeoutMs?: number },
|
|
550
|
+
): Promise<RemoteCompactionResponse> {
|
|
551
|
+
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: { "content-type": "application/json" },
|
|
554
|
+
body: JSON.stringify(request),
|
|
555
|
+
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
if (!response.ok) {
|
|
559
|
+
const errorText = await response.text().catch(() => "");
|
|
560
|
+
logger.warn("Remote compaction failed", {
|
|
561
|
+
endpoint,
|
|
562
|
+
status: response.status,
|
|
563
|
+
statusText: response.statusText,
|
|
564
|
+
errorText,
|
|
565
|
+
});
|
|
566
|
+
throw new ProviderHttpError(
|
|
567
|
+
`Remote compaction failed (${response.status} ${response.statusText})`,
|
|
568
|
+
response.status,
|
|
569
|
+
{
|
|
570
|
+
headers: response.headers,
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const data = (await response.json()) as RemoteCompactionResponse | undefined;
|
|
576
|
+
if (!data || typeof data.summary !== "string") {
|
|
577
|
+
throw new Error("Remote compaction response missing summary");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return data;
|
|
581
|
+
}
|
|
@@ -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 is the 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 model used. You MUST build on the work already done and NEVER duplicate it. Here is that summary:
|
|
2
|
+
|
|
3
|
+
<summary>
|
|
4
|
+
{{summary}}
|
|
5
|
+
</summary>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
You MUST summarize the conversation above into a structured handoff summary for another LLM to resume the task.
|
|
2
|
+
|
|
3
|
+
IMPORTANT: If the conversation ends with an unanswered question or a 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.
|