@opengeni/runtime 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
- package/dist/chunk-RYDCVTMW.js.map +1 -0
- package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
- package/dist/index.d.ts +293 -121
- package/dist/index.js +1819 -786
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +79 -3
- package/package.json +21 -20
- package/src/codex-tool-search.ts +185 -31
- package/src/context-compaction.ts +78 -243
- package/src/history-sanitizer.ts +172 -20
- package/src/index.ts +2573 -897
- package/src/metrics.ts +28 -2
- package/src/sandbox/capabilities.ts +12 -3
- package/src/sandbox/channel-a.ts +420 -72
- package/src/sandbox/display-stack.ts +18 -4
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +398 -112
- package/src/sandbox/op-correlation.ts +70 -0
- package/src/sandbox/providers/index.ts +9 -3
- package/src/sandbox/providers/modal.ts +99 -24
- package/src/sandbox/providers/selfhosted.ts +1 -4
- package/src/sandbox/recording.ts +146 -90
- package/src/sandbox/routing/backend-resolver.ts +144 -10
- package/src/sandbox/routing/routing-session.ts +129 -25
- package/src/sandbox/select.ts +60 -8
- package/src/sandbox/selfhosted/capabilities.ts +59 -25
- package/src/sandbox/selfhosted/control-rpc.ts +160 -28
- package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
- package/src/sandbox/selfhosted/op-observer.ts +54 -0
- package/src/sandbox/selfhosted/op-stream.ts +801 -0
- package/src/sandbox/selfhosted/op-testing.ts +468 -0
- package/src/sandbox/selfhosted/op-transport.ts +151 -0
- package/src/sandbox/selfhosted/retry-policy.ts +177 -0
- package/src/sandbox/selfhosted/session.ts +498 -55
- package/src/sandbox/selfhosted/testing.ts +60 -19
- package/src/sandbox/stream-port.ts +17 -5
- package/src/sandbox/stream-token.ts +8 -2
- package/src/sandbox/terminal-server.ts +18 -4
- package/src/sandbox-computer.ts +332 -70
- package/src/usage-telemetry.ts +73 -0
- package/dist/chunk-BHA2SU3H.js.map +0 -1
- package/src/image-history.ts +0 -149
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Portable conversation context compaction, following Codex CLI's local path.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The checkpoint model sees the current active history plus one fixed
|
|
5
|
+
* checkpoint prompt, then the active history is rebuilt from the newest real
|
|
6
|
+
* user messages within one cumulative 20k-token budget plus one summary.
|
|
7
7
|
* Assistant messages, tool calls/results, reasoning, and images are removed
|
|
8
8
|
* from the active model-facing history; the database audit rows remain.
|
|
9
9
|
*/
|
|
@@ -19,11 +19,15 @@ export type CompactionItem = Record<string, unknown>;
|
|
|
19
19
|
export const COMPACTION_SUMMARY_MARKER = "opengeni_context_summary";
|
|
20
20
|
|
|
21
21
|
export const SUMMARY_BUFFER_TOKENS = 20_000;
|
|
22
|
+
// A single cumulative budget for all retained real user messages, matching
|
|
23
|
+
// Codex core's build_compacted_history_with_limit (not a per-message allowance).
|
|
22
24
|
export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
|
|
23
|
-
|
|
25
|
+
// 0.9: compact as LATE as possible — retained context is worth more than early
|
|
26
|
+
// headroom now that declared per-model windows are honest. Model-catalog
|
|
27
|
+
// explicit limits take precedence; the ratio is used for models without one.
|
|
28
|
+
export const DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.9;
|
|
24
29
|
export const MIN_COMPACTION_THRESHOLD_RATIO = 0.3;
|
|
25
30
|
export const MAX_COMPACTION_THRESHOLD_RATIO = 0.9;
|
|
26
|
-
export const COMPACTION_FALLBACK_TARGET_RATIO = 0.5;
|
|
27
31
|
|
|
28
32
|
// Verbatim from Codex CLI:
|
|
29
33
|
// codex-rs/prompts/templates/compact/prompt.md
|
|
@@ -74,14 +78,18 @@ export function isUserMessage(item: unknown): boolean {
|
|
|
74
78
|
/** True for our synthetic compaction summary item. */
|
|
75
79
|
export function isCompactionSummary(item: unknown): boolean {
|
|
76
80
|
return (
|
|
77
|
-
isUserMessage(item)
|
|
78
|
-
&& (item as Record<string, unknown>)[COMPACTION_SUMMARY_MARKER] === true
|
|
81
|
+
isUserMessage(item) && (item as Record<string, unknown>)[COMPACTION_SUMMARY_MARKER] === true
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/** Platform-authored system context exists for one inference and is never persisted. */
|
|
86
|
+
export function isEphemeralInternalContext(item: unknown): boolean {
|
|
87
|
+
return itemType(item) === "message" && itemRole(item) === "system";
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
/**
|
|
83
91
|
* Rough token estimate for an item: char/4 over its serialized text. Used for
|
|
84
|
-
* the pre-first-call
|
|
92
|
+
* the pre-first-call signal and the retained user-message budget.
|
|
85
93
|
*/
|
|
86
94
|
export function estimateItemTokens(item: CompactionItem): number {
|
|
87
95
|
let text: string;
|
|
@@ -102,36 +110,51 @@ export function estimateTokens(items: readonly CompactionItem[]): number {
|
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
export function clampCompactionThresholdRatio(value: number | undefined | null): number {
|
|
105
|
-
const numeric =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
const numeric =
|
|
114
|
+
typeof value === "number" && Number.isFinite(value)
|
|
115
|
+
? value
|
|
116
|
+
: DEFAULT_COMPACTION_THRESHOLD_RATIO;
|
|
117
|
+
return Math.min(
|
|
118
|
+
MAX_COMPACTION_THRESHOLD_RATIO,
|
|
119
|
+
Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric),
|
|
120
|
+
);
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
export function
|
|
123
|
+
export function compactionThresholdTokens(input: {
|
|
112
124
|
contextWindowTokens: number;
|
|
113
125
|
contextReservedOutputTokens: number;
|
|
114
|
-
|
|
126
|
+
contextAutoCompactThresholdTokens?: number | null | undefined;
|
|
127
|
+
contextCompactionThresholdRatio?: number | null | undefined;
|
|
115
128
|
}): number {
|
|
116
|
-
|
|
129
|
+
const window = Math.max(0, input.contextWindowTokens);
|
|
130
|
+
const codexMaximum = Math.floor(window * MAX_COMPACTION_THRESHOLD_RATIO);
|
|
131
|
+
if (
|
|
132
|
+
typeof input.contextAutoCompactThresholdTokens === "number" &&
|
|
133
|
+
Number.isFinite(input.contextAutoCompactThresholdTokens) &&
|
|
134
|
+
input.contextAutoCompactThresholdTokens > 0
|
|
135
|
+
) {
|
|
136
|
+
return Math.min(Math.floor(input.contextAutoCompactThresholdTokens), codexMaximum);
|
|
137
|
+
}
|
|
138
|
+
return Math.floor(window * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
|
|
117
139
|
}
|
|
118
140
|
|
|
119
|
-
export type
|
|
141
|
+
export type CompactionDecision = {
|
|
120
142
|
shouldCompact: boolean;
|
|
121
143
|
reason: "force" | "above_threshold" | "below_threshold" | "no_history";
|
|
122
144
|
signalTokens: number;
|
|
123
145
|
thresholdTokens: number;
|
|
124
146
|
};
|
|
125
147
|
|
|
126
|
-
export function
|
|
148
|
+
export function decideCompaction(input: {
|
|
127
149
|
items: readonly CompactionItem[];
|
|
128
150
|
lastInputTokens?: number | null;
|
|
129
151
|
contextWindowTokens: number;
|
|
130
152
|
contextReservedOutputTokens: number;
|
|
131
|
-
|
|
153
|
+
contextAutoCompactThresholdTokens?: number | null | undefined;
|
|
154
|
+
contextCompactionThresholdRatio?: number | null | undefined;
|
|
132
155
|
force?: boolean;
|
|
133
|
-
}):
|
|
134
|
-
const thresholdTokens =
|
|
156
|
+
}): CompactionDecision {
|
|
157
|
+
const thresholdTokens = compactionThresholdTokens(input);
|
|
135
158
|
const recorded =
|
|
136
159
|
typeof input.lastInputTokens === "number" && input.lastInputTokens > 0
|
|
137
160
|
? input.lastInputTokens
|
|
@@ -143,7 +166,7 @@ export function decideClientCompaction(input: {
|
|
|
143
166
|
if (input.force) {
|
|
144
167
|
return { shouldCompact: true, reason: "force", signalTokens, thresholdTokens };
|
|
145
168
|
}
|
|
146
|
-
if (signalTokens
|
|
169
|
+
if (signalTokens >= thresholdTokens) {
|
|
147
170
|
return { shouldCompact: true, reason: "above_threshold", signalTokens, thresholdTokens };
|
|
148
171
|
}
|
|
149
172
|
return { shouldCompact: false, reason: "below_threshold", signalTokens, thresholdTokens };
|
|
@@ -153,23 +176,32 @@ export class CompactionNeededError extends Error {
|
|
|
153
176
|
readonly signalTokens: number;
|
|
154
177
|
readonly thresholdTokens: number;
|
|
155
178
|
readonly signalSource: "provider" | "estimate";
|
|
179
|
+
readonly trigger: "threshold" | "operator";
|
|
156
180
|
|
|
157
181
|
constructor(input: {
|
|
158
182
|
signalTokens: number;
|
|
159
183
|
thresholdTokens: number;
|
|
160
184
|
signalSource: "provider" | "estimate";
|
|
185
|
+
trigger?: "threshold" | "operator";
|
|
161
186
|
}) {
|
|
187
|
+
const trigger = input.trigger ?? "threshold";
|
|
162
188
|
super(
|
|
163
|
-
|
|
189
|
+
trigger === "operator"
|
|
190
|
+
? "Context compaction requested by the operator"
|
|
191
|
+
: `Context compaction needed: signal ${input.signalTokens} tokens exceeded threshold ${input.thresholdTokens}`,
|
|
164
192
|
);
|
|
165
193
|
this.name = "CompactionNeededError";
|
|
166
194
|
this.signalTokens = input.signalTokens;
|
|
167
195
|
this.thresholdTokens = input.thresholdTokens;
|
|
168
196
|
this.signalSource = input.signalSource;
|
|
197
|
+
this.trigger = trigger;
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
200
|
|
|
172
|
-
export function findCompactionNeededError(
|
|
201
|
+
export function findCompactionNeededError(
|
|
202
|
+
error: unknown,
|
|
203
|
+
seen = new WeakSet<object>(),
|
|
204
|
+
): CompactionNeededError | null {
|
|
173
205
|
if (error instanceof CompactionNeededError) {
|
|
174
206
|
return error;
|
|
175
207
|
}
|
|
@@ -182,66 +214,10 @@ export function findCompactionNeededError(error: unknown, seen = new WeakSet<obj
|
|
|
182
214
|
seen.add(error);
|
|
183
215
|
const record = error as Record<string, unknown>;
|
|
184
216
|
return (
|
|
185
|
-
findCompactionNeededError(record.cause, seen)
|
|
186
|
-
?? findCompactionNeededError(record.error, seen)
|
|
217
|
+
findCompactionNeededError(record.cause, seen) ?? findCompactionNeededError(record.error, seen)
|
|
187
218
|
);
|
|
188
219
|
}
|
|
189
220
|
|
|
190
|
-
/**
|
|
191
|
-
* Walk backwards from the end of `items` keeping whole turns until the kept
|
|
192
|
-
* tail would exceed `keepRecentTokens`, and return the index of the first kept
|
|
193
|
-
* item. Retained for the read-path budget guard only; the client compaction
|
|
194
|
-
* rebuild no longer uses a keep-recent tail.
|
|
195
|
-
*/
|
|
196
|
-
export function findKeepBoundary(items: readonly CompactionItem[], keepRecentTokens: number): number {
|
|
197
|
-
const boundaries: number[] = [];
|
|
198
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
199
|
-
if (isUserMessage(items[i])) {
|
|
200
|
-
boundaries.push(i);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (boundaries.length === 0) {
|
|
204
|
-
return 0;
|
|
205
|
-
}
|
|
206
|
-
for (let b = 0; b < boundaries.length; b += 1) {
|
|
207
|
-
const boundary = boundaries[b]!;
|
|
208
|
-
if (estimateTokens(items.slice(boundary)) <= keepRecentTokens) {
|
|
209
|
-
return boundary;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return boundaries[boundaries.length - 1]!;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* READ-PATH BUDGET GUARD (last-resort backstop).
|
|
217
|
-
*
|
|
218
|
-
* Drops the oldest history at a clean user-message boundary until an assembled
|
|
219
|
-
* input fits the request budget. This remains a request-local safety rail; it
|
|
220
|
-
* is not the compaction strategy.
|
|
221
|
-
*/
|
|
222
|
-
export function enforceInputBudget<T extends CompactionItem>(
|
|
223
|
-
items: readonly T[],
|
|
224
|
-
maxTokens: number,
|
|
225
|
-
trailingTokens = 0,
|
|
226
|
-
): { items: T[]; trimmed: boolean; droppedCount: number; estimatedTokens: number } {
|
|
227
|
-
const total = estimateTokens(items) + Math.max(0, trailingTokens);
|
|
228
|
-
if (items.length === 0 || total <= maxTokens) {
|
|
229
|
-
return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
|
|
230
|
-
}
|
|
231
|
-
const historyBudget = Math.max(0, maxTokens - Math.max(0, trailingTokens));
|
|
232
|
-
const boundary = findKeepBoundary(items, historyBudget);
|
|
233
|
-
if (boundary <= 0) {
|
|
234
|
-
return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
|
|
235
|
-
}
|
|
236
|
-
const kept = items.slice(boundary);
|
|
237
|
-
return {
|
|
238
|
-
items: kept,
|
|
239
|
-
trimmed: true,
|
|
240
|
-
droppedCount: boundary,
|
|
241
|
-
estimatedTokens: estimateTokens(kept) + Math.max(0, trailingTokens),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
221
|
/**
|
|
246
222
|
* The exact checkpoint input shape: current active history followed by Codex's
|
|
247
223
|
* checkpoint prompt as a synthesized user message.
|
|
@@ -259,86 +235,31 @@ export function buildCompactionPromptInput(items: readonly CompactionItem[]): Co
|
|
|
259
235
|
|
|
260
236
|
/**
|
|
261
237
|
* Build the active history after compaction:
|
|
262
|
-
*
|
|
263
|
-
*
|
|
238
|
+
* the newest real user messages that fit one cumulative 20k-token budget
|
|
239
|
+
* (prior summaries excluded, images removed) plus one marked summary item.
|
|
264
240
|
*/
|
|
265
241
|
export function buildCompactionReplacementHistory(
|
|
266
242
|
items: readonly CompactionItem[],
|
|
267
243
|
summaryBody: string,
|
|
268
244
|
): CompactionItem[] {
|
|
269
|
-
const
|
|
270
|
-
|
|
245
|
+
const retainedReversed: CompactionItem[] = [];
|
|
246
|
+
let remaining = COMPACT_USER_MESSAGE_MAX_TOKENS;
|
|
247
|
+
for (let index = items.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
|
248
|
+
const item = items[index]!;
|
|
271
249
|
if (!isUserMessage(item) || isCompactionSummary(item)) {
|
|
272
250
|
continue;
|
|
273
251
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export type DeterministicFallbackCompactionInput = {
|
|
281
|
-
items: readonly CompactionItem[];
|
|
282
|
-
cause?: string;
|
|
283
|
-
targetTokens?: number;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
export function buildDeterministicFallbackCompactionHistory(input: DeterministicFallbackCompactionInput): CompactionItem[] {
|
|
287
|
-
const sourceTokens = Math.max(1, estimateTokens(input.items));
|
|
288
|
-
const requestedTarget = typeof input.targetTokens === "number" && input.targetTokens > 0
|
|
289
|
-
? Math.floor(input.targetTokens)
|
|
290
|
-
: Math.floor(sourceTokens * COMPACTION_FALLBACK_TARGET_RATIO);
|
|
291
|
-
const targetTokens = Math.max(1, Math.min(requestedTarget, Math.max(1, sourceTokens - 1)));
|
|
292
|
-
const summaryItem = buildSummaryItem([
|
|
293
|
-
"Non-LLM context compaction fallback.",
|
|
294
|
-
input.cause ? `Summarizer failure: ${input.cause}` : "Summarizer failure: unavailable.",
|
|
295
|
-
"Older assistant, tool, reasoning, and image history was dropped deterministically.",
|
|
296
|
-
"Retained context above is limited to initial instructions and the most recent user messages that fit the fallback budget; oversized retained messages may be middle-truncated.",
|
|
297
|
-
].join("\n"));
|
|
298
|
-
const summaryTokens = estimateItemTokens(summaryItem);
|
|
299
|
-
let remaining = Math.max(0, targetTokens - summaryTokens);
|
|
300
|
-
const retainedInstructions: CompactionItem[] = [];
|
|
301
|
-
|
|
302
|
-
for (const item of input.items) {
|
|
303
|
-
if (!isInitialInstructionMessage(item)) {
|
|
304
|
-
if (isUserMessage(item)) {
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if (remaining <= 0) {
|
|
252
|
+
const textTokens = estimatedTextTokens(messageText(item));
|
|
253
|
+
retainedReversed.push(compactMessageToTokenBudget(item, remaining));
|
|
254
|
+
if (textTokens > remaining) {
|
|
255
|
+
remaining = 0;
|
|
310
256
|
break;
|
|
311
257
|
}
|
|
312
|
-
|
|
313
|
-
if (!capped) {
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
const tokens = estimateItemTokens(capped);
|
|
317
|
-
if (tokens <= remaining) {
|
|
318
|
-
retainedInstructions.push(capped);
|
|
319
|
-
remaining -= tokens;
|
|
320
|
-
}
|
|
258
|
+
remaining -= textTokens;
|
|
321
259
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const item = input.items[index]!;
|
|
326
|
-
if (!isUserMessage(item) || isCompactionSummary(item)) {
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
const capped = compactMessageToEstimatedItemBudget(item, Math.min(COMPACT_USER_MESSAGE_MAX_TOKENS, remaining));
|
|
330
|
-
if (!capped) {
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const tokens = estimateItemTokens(capped);
|
|
334
|
-
if (tokens <= remaining) {
|
|
335
|
-
retainedUsers.push(capped);
|
|
336
|
-
remaining -= tokens;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
retainedUsers.reverse();
|
|
340
|
-
|
|
341
|
-
return [...retainedInstructions, ...retainedUsers, summaryItem];
|
|
260
|
+
const history = retainedReversed.reverse();
|
|
261
|
+
history.push(buildSummaryItem(summaryBody));
|
|
262
|
+
return history;
|
|
342
263
|
}
|
|
343
264
|
|
|
344
265
|
/**
|
|
@@ -346,7 +267,7 @@ export function buildDeterministicFallbackCompactionHistory(input: Deterministic
|
|
|
346
267
|
* rebuilt active history.
|
|
347
268
|
*/
|
|
348
269
|
export function buildSummaryItem(summaryBody: string): CompactionItem {
|
|
349
|
-
const trimmed = summaryBody.trim();
|
|
270
|
+
const trimmed = summaryBody.trim() || "(no summary available)";
|
|
350
271
|
return {
|
|
351
272
|
type: "message",
|
|
352
273
|
role: "user",
|
|
@@ -355,10 +276,6 @@ export function buildSummaryItem(summaryBody: string): CompactionItem {
|
|
|
355
276
|
};
|
|
356
277
|
}
|
|
357
278
|
|
|
358
|
-
function compactUserMessage(item: CompactionItem): CompactionItem {
|
|
359
|
-
return compactMessageToTokenBudget(item, COMPACT_USER_MESSAGE_MAX_TOKENS);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
279
|
function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): CompactionItem {
|
|
363
280
|
const text = messageText(item);
|
|
364
281
|
const next = { ...item };
|
|
@@ -370,27 +287,6 @@ function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): C
|
|
|
370
287
|
return next;
|
|
371
288
|
}
|
|
372
289
|
|
|
373
|
-
function compactMessageToEstimatedItemBudget(item: CompactionItem, maxItemTokens: number): CompactionItem | null {
|
|
374
|
-
if (maxItemTokens <= 0) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
const overheadTokens = estimateItemTokens({ ...item, content: "" });
|
|
378
|
-
let contentBudget = Math.max(0, maxItemTokens - overheadTokens);
|
|
379
|
-
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
380
|
-
const capped = compactMessageToTokenBudget(item, contentBudget);
|
|
381
|
-
const estimated = estimateItemTokens(capped);
|
|
382
|
-
if (estimated <= maxItemTokens) {
|
|
383
|
-
return capped;
|
|
384
|
-
}
|
|
385
|
-
const overage = estimated - maxItemTokens;
|
|
386
|
-
if (contentBudget <= 0) {
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
contentBudget = Math.max(0, contentBudget - Math.max(1, overage));
|
|
390
|
-
}
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
290
|
function estimatedTextTokens(text: string): number {
|
|
395
291
|
return Math.ceil(text.length / 4);
|
|
396
292
|
}
|
|
@@ -447,60 +343,26 @@ function messageText(item: CompactionItem): string {
|
|
|
447
343
|
return "";
|
|
448
344
|
}
|
|
449
345
|
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
const role = itemRole(item);
|
|
453
|
-
return type === "message" && (role === "system" || role === "developer");
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export type RenderCompactionTranscriptOptions = {
|
|
457
|
-
/**
|
|
458
|
-
* Hard cap for a rendered transcript. When set, oldest rendered items are
|
|
459
|
-
* dropped first while preserving the final checkpoint prompt; if the retained
|
|
460
|
-
* item itself is too large, it is middle-truncated instead of looping.
|
|
461
|
-
*/
|
|
462
|
-
maxEstimatedTokens?: number;
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
export function renderCompactionPromptInputForChat(
|
|
466
|
-
input: readonly CompactionItem[],
|
|
467
|
-
options: RenderCompactionTranscriptOptions = {},
|
|
468
|
-
): string {
|
|
469
|
-
const rendered = input.map(renderItem);
|
|
470
|
-
if (typeof options.maxEstimatedTokens !== "number" || options.maxEstimatedTokens <= 0) {
|
|
471
|
-
return rendered.join("\n");
|
|
472
|
-
}
|
|
473
|
-
const maxChars = Math.max(1, Math.floor(options.maxEstimatedTokens * 4));
|
|
474
|
-
const last = rendered.at(-1);
|
|
475
|
-
const prefix = rendered.slice(0, -1);
|
|
476
|
-
let kept = prefix.slice();
|
|
477
|
-
while (kept.length > 0 && transcriptLength([...kept, last].filter((line): line is string => line !== undefined)) > maxChars) {
|
|
478
|
-
kept.shift();
|
|
479
|
-
}
|
|
480
|
-
const lines = [...kept, last].filter((line): line is string => line !== undefined);
|
|
481
|
-
const joined = lines.join("\n");
|
|
482
|
-
if (joined.length <= maxChars) {
|
|
483
|
-
return joined;
|
|
484
|
-
}
|
|
485
|
-
return truncateMiddleByChars(joined, maxChars);
|
|
346
|
+
export function renderCompactionPromptInputForChat(input: readonly CompactionItem[]): string {
|
|
347
|
+
return input.map(renderItem).join("\n");
|
|
486
348
|
}
|
|
487
349
|
|
|
488
350
|
function renderItem(item: CompactionItem): string {
|
|
489
351
|
const type = itemType(item) ?? "unknown";
|
|
490
352
|
if (type === "message") {
|
|
491
353
|
const role = itemRole(item) ?? "assistant";
|
|
492
|
-
return `[${role}] ${
|
|
354
|
+
return `[${role}] ${messageText(item)}`;
|
|
493
355
|
}
|
|
494
356
|
if (type === "reasoning") {
|
|
495
357
|
return "[reasoning] (omitted)";
|
|
496
358
|
}
|
|
497
359
|
if (RESULT_TYPES.has(type)) {
|
|
498
|
-
return `[tool_result] ${
|
|
360
|
+
return `[tool_result] ${resultText(item)}`;
|
|
499
361
|
}
|
|
500
362
|
if (RESULT_TYPE_BY_CALL_TYPE[type]) {
|
|
501
|
-
return `[tool_call ${type}] ${
|
|
363
|
+
return `[tool_call ${type}] ${callText(item)}`;
|
|
502
364
|
}
|
|
503
|
-
return `[${type}] ${
|
|
365
|
+
return `[${type}] ${safeStringify(item)}`;
|
|
504
366
|
}
|
|
505
367
|
|
|
506
368
|
function resultText(item: CompactionItem): string {
|
|
@@ -526,30 +388,3 @@ function safeStringify(value: unknown): string {
|
|
|
526
388
|
return String(value);
|
|
527
389
|
}
|
|
528
390
|
}
|
|
529
|
-
|
|
530
|
-
function truncateForTranscript(text: string, max: number): string {
|
|
531
|
-
if (text.length <= max) {
|
|
532
|
-
return text;
|
|
533
|
-
}
|
|
534
|
-
return `${text.slice(0, max)}... (${text.length - max} more chars)`;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function transcriptLength(lines: readonly string[]): number {
|
|
538
|
-
if (lines.length === 0) {
|
|
539
|
-
return 0;
|
|
540
|
-
}
|
|
541
|
-
return lines.reduce((sum, line) => sum + line.length, 0) + Math.max(0, lines.length - 1);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function truncateMiddleByChars(text: string, maxChars: number): string {
|
|
545
|
-
if (text.length <= maxChars) {
|
|
546
|
-
return text;
|
|
547
|
-
}
|
|
548
|
-
if (maxChars <= USER_MESSAGE_TRUNCATION_MARKER.length) {
|
|
549
|
-
return USER_MESSAGE_TRUNCATION_MARKER.slice(0, maxChars);
|
|
550
|
-
}
|
|
551
|
-
const keepChars = maxChars - USER_MESSAGE_TRUNCATION_MARKER.length;
|
|
552
|
-
const headChars = Math.ceil(keepChars / 2);
|
|
553
|
-
const tailChars = Math.floor(keepChars / 2);
|
|
554
|
-
return `${text.slice(0, headChars)}${USER_MESSAGE_TRUNCATION_MARKER}${text.slice(text.length - tailChars)}`;
|
|
555
|
-
}
|