@linxiraos/pi-agent-core 1.2.1 → 1.3.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/CHANGELOG.md +40 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/agent.d.ts +15 -5
- package/dist/types/compaction/compaction.d.ts +20 -4
- package/dist/types/compaction/openai.d.ts +13 -2
- package/dist/types/compaction/shake.d.ts +7 -5
- package/dist/types/compaction/utils.d.ts +7 -2
- package/dist/types/image-tokens.d.ts +9 -12
- package/dist/types/output-budget.d.ts +17 -7
- package/dist/types/tokenizer.d.ts +72 -12
- package/dist/types/types.d.ts +22 -1
- package/package.json +9 -9
- package/src/agent-loop.ts +91 -27
- package/src/agent.ts +56 -22
- package/src/compaction/branch-summarization.ts +28 -3
- package/src/compaction/compaction.ts +286 -149
- package/src/compaction/openai.ts +97 -49
- package/src/compaction/shake.ts +19 -8
- package/src/compaction/utils.ts +8 -10
- package/src/image-tokens.ts +13 -15
- package/src/output-budget.ts +22 -9
- package/src/tokenizer.ts +238 -27
- package/src/types.ts +24 -1
|
@@ -34,11 +34,13 @@ import {
|
|
|
34
34
|
} from "@linxiraos/pi-ai/providers/openai-codex-responses";
|
|
35
35
|
import { convertTools } from "@linxiraos/pi-ai/providers/openai-responses";
|
|
36
36
|
import { buildResponsesInput, resolveOpenAICompatPolicy } from "@linxiraos/pi-ai/providers/openai-shared";
|
|
37
|
+
import { isSurvivingAssistant } from "@linxiraos/pi-ai/providers/transform-messages";
|
|
37
38
|
import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@linxiraos/pi-ai/utils";
|
|
39
|
+
import { resolveCatalogPolicy } from "@linxiraos/pi-catalog/compat/catalog-policy";
|
|
38
40
|
import { preferredDialect } from "@linxiraos/pi-catalog/identity";
|
|
39
41
|
import { clampThinkingLevelForModel } from "@linxiraos/pi-catalog/model-thinking";
|
|
40
|
-
import * as snapcompact from "@linxiraos/pi-snapcompact";
|
|
41
42
|
import { isRecord, logger, prompt } from "@linxiraos/pi-utils";
|
|
43
|
+
import * as snapcompact from "@linxiraos/pi-snapcompact";
|
|
42
44
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
43
45
|
import { ThinkingLevel } from "../thinking";
|
|
44
46
|
import { Tokenizer } from "../tokenizer";
|
|
@@ -152,6 +154,12 @@ export interface CompactionResult<T = unknown> {
|
|
|
152
154
|
details?: T;
|
|
153
155
|
/** Hook-provided data to persist alongside compaction entry. */
|
|
154
156
|
preserveData?: Record<string, unknown>;
|
|
157
|
+
/**
|
|
158
|
+
* Last entry the provider-native replay history covers. Set when a remote
|
|
159
|
+
* compaction could only fit the history before the kept tail; later entries
|
|
160
|
+
* replay normally after the native history.
|
|
161
|
+
*/
|
|
162
|
+
providerReplayThroughEntryId?: string;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
// ============================================================================
|
|
@@ -369,9 +377,11 @@ export function compactionContextTokens(providerContextTokens: number, storedCon
|
|
|
369
377
|
/**
|
|
370
378
|
* The prompt budget below `contextWindow`: the window less
|
|
371
379
|
* {@link resolveBudgetReserveTokens}, capped at `contextWindow - 1` so it never
|
|
372
|
-
* reaches the whole window even when the reserve resolves to 0.
|
|
380
|
+
* reaches the whole window even when the reserve resolves to 0. Independent of
|
|
381
|
+
* any configured trigger; the default {@link resolveThresholdTokens} and
|
|
382
|
+
* length-stop recovery's "did the window run out" check both use it.
|
|
373
383
|
*/
|
|
374
|
-
function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
384
|
+
export function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
375
385
|
return Math.max(0, Math.min(contextWindow - 1, contextWindow - resolveBudgetReserveTokens(contextWindow, settings)));
|
|
376
386
|
}
|
|
377
387
|
|
|
@@ -529,13 +539,23 @@ export function findCutPoint(
|
|
|
529
539
|
// belonging to an assistant. Checking individual messages can either retain
|
|
530
540
|
// the oversized older assistant or miss its boundary and retain all history.
|
|
531
541
|
let accumulatedTokens = 0;
|
|
542
|
+
// The latest surviving assistant turn replays byte-for-byte, signatures of
|
|
543
|
+
// abandoned tool calls included, so it is counted as the latest.
|
|
544
|
+
let latestTurnIndex = -1;
|
|
545
|
+
for (let i = entries.length - 1; i >= 0 && latestTurnIndex < 0; i--) {
|
|
546
|
+
const message = getMessageFromEntry(entries[i]);
|
|
547
|
+
if (message?.role === "assistant" && isSurvivingAssistant(message)) latestTurnIndex = i;
|
|
548
|
+
}
|
|
532
549
|
let cutPointIndex = cutPoints.length - 1;
|
|
533
550
|
let cutIndex = cutPoints[cutPointIndex];
|
|
534
551
|
|
|
535
552
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
536
553
|
const entry = entries[i];
|
|
537
554
|
const message = getMessageFromEntry(entry);
|
|
538
|
-
if (message)
|
|
555
|
+
if (message) {
|
|
556
|
+
accumulatedTokens +=
|
|
557
|
+
i === latestTurnIndex ? tokenizer.countLatestTurn(message) : tokenizer.countMessage(message);
|
|
558
|
+
}
|
|
539
559
|
if (i !== cutPoints[cutPointIndex]) continue;
|
|
540
560
|
if (accumulatedTokens > keepRecentTokens) break;
|
|
541
561
|
cutIndex = i;
|
|
@@ -673,14 +693,15 @@ export interface SummaryOptions {
|
|
|
673
693
|
convertToLlm?: ConvertToLlm;
|
|
674
694
|
/**
|
|
675
695
|
* The provider context a live turn sends for `summarized` + `retained`, cut to
|
|
676
|
-
* `summarized
|
|
677
|
-
* thinking keeps its prefix; without
|
|
678
|
-
* `tools` and `convertToLlm`.
|
|
696
|
+
* `summarized`, as read by the summarizer `model`. Anthropic on-demand
|
|
697
|
+
* compaction sends it so retained signed thinking keeps its prefix; without
|
|
698
|
+
* it the request uses `remoteSystemPrompt`, `tools` and `convertToLlm`.
|
|
679
699
|
*/
|
|
680
700
|
buildProviderContext?: (
|
|
681
701
|
summarized: AgentMessage[],
|
|
682
702
|
retained: AgentMessage[],
|
|
683
|
-
signal
|
|
703
|
+
signal: AbortSignal | undefined,
|
|
704
|
+
model: Model,
|
|
684
705
|
) => Promise<Context>;
|
|
685
706
|
/**
|
|
686
707
|
* Whether a message is a turn the user wrote. Remote Compaction V2 keeps these
|
|
@@ -850,13 +871,14 @@ function planSummaryWindows(
|
|
|
850
871
|
messages: Message[],
|
|
851
872
|
tokenizer: Tokenizer,
|
|
852
873
|
dialect: Dialect | undefined,
|
|
874
|
+
dropThinking: boolean,
|
|
853
875
|
budgetTokens: number,
|
|
854
876
|
): Message[][] {
|
|
855
877
|
const windows: Message[][] = [];
|
|
856
878
|
let current: Message[] = [];
|
|
857
879
|
let currentTokens = 0;
|
|
858
880
|
for (const message of messages) {
|
|
859
|
-
const tokens = tokenizer.countTokens(serializeConversationForSummary([message], dialect));
|
|
881
|
+
const tokens = tokenizer.countTokens(serializeConversationForSummary([message], dialect, dropThinking));
|
|
860
882
|
if (currentTokens > 0 && currentTokens + tokens > budgetTokens) {
|
|
861
883
|
windows.push(current);
|
|
862
884
|
current = [];
|
|
@@ -885,8 +907,9 @@ export async function generateSummary(
|
|
|
885
907
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
886
908
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
|
|
887
909
|
const dialect = preferredDialect(model.id);
|
|
910
|
+
const dropThinking = resolveCatalogPolicy(model).classifierCotExtraction === true;
|
|
888
911
|
const tokenizer = new Tokenizer(model);
|
|
889
|
-
const wholeConversation = serializeConversationForSummary(llmMessages, dialect);
|
|
912
|
+
const wholeConversation = serializeConversationForSummary(llmMessages, dialect, dropThinking);
|
|
890
913
|
const budgetTokens = summaryInputBudgetTokens(model, maxTokens);
|
|
891
914
|
// A span that outgrew the summarizer's window is summarized as a fold: each
|
|
892
915
|
// window updates the summary carried out of the previous one, which is the
|
|
@@ -897,12 +920,15 @@ export async function generateSummary(
|
|
|
897
920
|
// common case and costs exactly the one call it always did.
|
|
898
921
|
const pending: SummaryWindow[] = tokenizer.checkTokenBudget(wholeConversation, budgetTokens).fits
|
|
899
922
|
? [{ messages: llmMessages, budgetTokens, text: wholeConversation }]
|
|
900
|
-
: planSummaryWindows(llmMessages, tokenizer, dialect, budgetTokens).map(messages => ({
|
|
923
|
+
: planSummaryWindows(llmMessages, tokenizer, dialect, dropThinking, budgetTokens).map(messages => ({
|
|
924
|
+
messages,
|
|
925
|
+
budgetTokens,
|
|
926
|
+
}));
|
|
901
927
|
|
|
902
928
|
let carriedSummary = previousSummary;
|
|
903
929
|
while (pending.length > 0) {
|
|
904
930
|
const window = pending[0];
|
|
905
|
-
const text = window.text ?? serializeConversationForSummary(window.messages, dialect);
|
|
931
|
+
const text = window.text ?? serializeConversationForSummary(window.messages, dialect, dropThinking);
|
|
906
932
|
// A budget probe, not a raw count: a window whose bytes already fit needs
|
|
907
933
|
// neither an exact count nor the clamp, and the bust path hands back the
|
|
908
934
|
// exact count the proportional clamp needs as its denominator.
|
|
@@ -940,7 +966,7 @@ export async function generateSummary(
|
|
|
940
966
|
pending.splice(
|
|
941
967
|
0,
|
|
942
968
|
1,
|
|
943
|
-
...planSummaryWindows(window.messages, tokenizer, dialect, halved).map(messages => ({
|
|
969
|
+
...planSummaryWindows(window.messages, tokenizer, dialect, dropThinking, halved).map(messages => ({
|
|
944
970
|
messages,
|
|
945
971
|
budgetTokens: halved,
|
|
946
972
|
})),
|
|
@@ -1190,7 +1216,11 @@ async function generateShortSummary(
|
|
|
1190
1216
|
): Promise<string> {
|
|
1191
1217
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
1192
1218
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(recentMessages);
|
|
1193
|
-
const conversationText = serializeConversationForSummary(
|
|
1219
|
+
const conversationText = serializeConversationForSummary(
|
|
1220
|
+
llmMessages,
|
|
1221
|
+
preferredDialect(model.id),
|
|
1222
|
+
resolveCatalogPolicy(model).classifierCotExtraction === true,
|
|
1223
|
+
);
|
|
1194
1224
|
|
|
1195
1225
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
1196
1226
|
if (historySummary) {
|
|
@@ -1267,6 +1297,8 @@ export interface CompactionPreparation {
|
|
|
1267
1297
|
recentMessages: AgentMessage[];
|
|
1268
1298
|
/** Entry IDs parallel to recentMessages, for an Anthropic-safe keep-tail boundary. */
|
|
1269
1299
|
recentEntryIds?: string[];
|
|
1300
|
+
/** Last entry before the kept tail; a remote compaction that cannot fit the tail covers history through it. */
|
|
1301
|
+
lastSummarizedEntryId?: string;
|
|
1270
1302
|
/** Whether this is a split turn (cut point in middle of turn) */
|
|
1271
1303
|
isSplitTurn: boolean;
|
|
1272
1304
|
tokensBefore: number;
|
|
@@ -1479,6 +1511,7 @@ export function prepareCompaction(
|
|
|
1479
1511
|
turnPrefixMessages,
|
|
1480
1512
|
recentMessages,
|
|
1481
1513
|
recentEntryIds,
|
|
1514
|
+
lastSummarizedEntryId: compactionEntries[cutPoint.firstKeptEntryIndex - 1]?.id,
|
|
1482
1515
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
1483
1516
|
tokensBefore,
|
|
1484
1517
|
previousSummary: previousCompaction?.summary,
|
|
@@ -1586,6 +1619,13 @@ function formatRemoteCompactionSummary(inputTokens: number): string {
|
|
|
1586
1619
|
);
|
|
1587
1620
|
}
|
|
1588
1621
|
|
|
1622
|
+
/** A V2 remote-compaction request's history and the user items its replacement history retains. */
|
|
1623
|
+
interface RemoteCompactionV2Input {
|
|
1624
|
+
codexBody: OpenAICodexCompactionBody | undefined;
|
|
1625
|
+
remoteHistory: Array<Record<string, unknown>>;
|
|
1626
|
+
retainedUserItems: unknown[];
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1589
1629
|
/**
|
|
1590
1630
|
* Generate summaries for compaction using prepared data.
|
|
1591
1631
|
* Returns CompactionResult - SessionManager adds id/parentId when saving.
|
|
@@ -1607,6 +1647,7 @@ export async function compact(
|
|
|
1607
1647
|
turnPrefixMessages,
|
|
1608
1648
|
recentMessages,
|
|
1609
1649
|
recentEntryIds,
|
|
1650
|
+
lastSummarizedEntryId,
|
|
1610
1651
|
isSplitTurn,
|
|
1611
1652
|
tokensBefore,
|
|
1612
1653
|
previousSummary,
|
|
@@ -1647,12 +1688,28 @@ export async function compact(
|
|
|
1647
1688
|
oneshotRetry: options?.oneshotRetry,
|
|
1648
1689
|
};
|
|
1649
1690
|
|
|
1650
|
-
|
|
1691
|
+
// An archive keeps the reasoning policy of the model it was rendered for;
|
|
1692
|
+
// Claude refuses inputs that reproduce reasoning (issue #6093), so a
|
|
1693
|
+
// summarizer whose classifier refuses quoted reasoning (the
|
|
1694
|
+
// `classifier-cot-extraction` catalog axis) reads the archive's no-reasoning view (scrubbed
|
|
1695
|
+
// while its tool-output spans are still marked), and a summary with no
|
|
1696
|
+
// archive behind it is scrubbed of `¶think:` sections.
|
|
1697
|
+
const withoutThinking = !snapcompact.readerIncludesThinking(model);
|
|
1698
|
+
const storedSnapcompactArchive = snapcompact.getPreservedArchive(previousPreserveData);
|
|
1699
|
+
const snapcompactView =
|
|
1700
|
+
storedSnapcompactArchive && withoutThinking
|
|
1701
|
+
? snapcompact.viewWithoutThinking(previousSummary ?? "", storedSnapcompactArchive)
|
|
1702
|
+
: undefined;
|
|
1703
|
+
const previousSnapcompactArchive = snapcompactView?.archive ?? storedSnapcompactArchive;
|
|
1704
|
+
const previousSummaryForReader =
|
|
1705
|
+
withoutThinking && previousSummary !== undefined
|
|
1706
|
+
? (snapcompactView?.summary ?? snapcompact.textWithoutThinking(previousSummary))
|
|
1707
|
+
: previousSummary;
|
|
1651
1708
|
const previousSnapcompactArchiveText = previousSnapcompactArchive
|
|
1652
1709
|
? snapcompact.archiveSourceText(previousSnapcompactArchive)
|
|
1653
1710
|
: undefined;
|
|
1654
1711
|
const previousSummaryForCompaction = mergePreviousSummaryWithSnapcompactArchive(
|
|
1655
|
-
|
|
1712
|
+
previousSummaryForReader,
|
|
1656
1713
|
previousSnapcompactArchiveText,
|
|
1657
1714
|
);
|
|
1658
1715
|
const snapcompactArchiveMigrationMessage = previousSnapcompactArchiveText
|
|
@@ -1663,23 +1720,58 @@ export async function compact(
|
|
|
1663
1720
|
// A local summary has no native payload to carry it into the first remote
|
|
1664
1721
|
// request. Encode it as history; do not resend opaque native placeholders.
|
|
1665
1722
|
const previousSummaryMigrationMessage =
|
|
1666
|
-
settings.remoteEnabled !== false &&
|
|
1667
|
-
? createCompactionSummaryMessage(
|
|
1723
|
+
settings.remoteEnabled !== false && previousSummaryForReader && !previousNativeHistory
|
|
1724
|
+
? createCompactionSummaryMessage(previousSummaryForReader, tokensBefore, new Date().toISOString())
|
|
1668
1725
|
: undefined;
|
|
1669
1726
|
|
|
1670
1727
|
let preserveData = withAnthropicCompactionPreserveData(
|
|
1671
1728
|
withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined),
|
|
1672
1729
|
undefined,
|
|
1673
1730
|
);
|
|
1674
|
-
const
|
|
1731
|
+
const remotePrefixMessages: AgentMessage[] = [
|
|
1675
1732
|
...(previousSummaryMigrationMessage ? [previousSummaryMigrationMessage] : []),
|
|
1676
1733
|
...(snapcompactArchiveMigrationMessage ? [snapcompactArchiveMigrationMessage] : []),
|
|
1677
1734
|
...messagesToSummarize,
|
|
1678
1735
|
...turnPrefixMessages,
|
|
1679
|
-
...recentMessages,
|
|
1680
1736
|
];
|
|
1737
|
+
const remoteMessages: AgentMessage[] = [...remotePrefixMessages, ...recentMessages];
|
|
1681
1738
|
let usedRemoteCompaction = false;
|
|
1739
|
+
let remoteReplayThroughEntryId: string | undefined;
|
|
1682
1740
|
let nativeCompactionError: unknown;
|
|
1741
|
+
/**
|
|
1742
|
+
* Remote compaction replaces the whole history, so its request carries the
|
|
1743
|
+
* kept tail too. A turn can leave that history over the window (a large
|
|
1744
|
+
* response or tool output after the last check), which no request can
|
|
1745
|
+
* send. Then compact the history before the tail instead; the tail replays
|
|
1746
|
+
* normally after the native history, as a speculative snapshot's later
|
|
1747
|
+
* turns do. While that fallback exists, the whole-history attempt must not
|
|
1748
|
+
* rewrite the tail's trailing tool outputs: the prefix keeps them intact.
|
|
1749
|
+
*/
|
|
1750
|
+
const canCompactBeforeTail = lastSummarizedEntryId !== undefined && recentMessages.length > 0;
|
|
1751
|
+
const sendRemoteCompaction = async (
|
|
1752
|
+
sendWhole: () => Promise<void>,
|
|
1753
|
+
sendPrefix: () => Promise<boolean>,
|
|
1754
|
+
): Promise<void> => {
|
|
1755
|
+
try {
|
|
1756
|
+
await sendWhole();
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
if (
|
|
1759
|
+
signal?.aborted ||
|
|
1760
|
+
!canCompactBeforeTail ||
|
|
1761
|
+
!AIError.is(AIError.classify(err), AIError.Flag.ContextOverflow)
|
|
1762
|
+
) {
|
|
1763
|
+
throw err;
|
|
1764
|
+
}
|
|
1765
|
+
logger.info("Remote compaction history overflows the context window; compacting before the kept tail", {
|
|
1766
|
+
model: model.id,
|
|
1767
|
+
provider: model.provider,
|
|
1768
|
+
keptMessages: recentMessages.length,
|
|
1769
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1770
|
+
});
|
|
1771
|
+
if (!(await sendPrefix())) throw err;
|
|
1772
|
+
remoteReplayThroughEntryId = lastSummarizedEntryId;
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1683
1775
|
if (
|
|
1684
1776
|
settings.remoteEnabled !== false &&
|
|
1685
1777
|
settings.remoteStreamingV2Enabled !== false &&
|
|
@@ -1691,126 +1783,147 @@ export async function compact(
|
|
|
1691
1783
|
? previousRemoteCompaction.replacementHistory
|
|
1692
1784
|
: undefined;
|
|
1693
1785
|
const convertToLlm = summaryOptions.convertToLlm ?? defaultConvertToLlm;
|
|
1694
|
-
const messages = convertToLlm(remoteMessages);
|
|
1695
1786
|
// Replacement history keeps what the user wrote. Summaries, archive
|
|
1696
1787
|
// migrations, and custom/hook messages can serialize as user-role items
|
|
1697
1788
|
// too, so pick the user's own messages before serialization erases that,
|
|
1698
1789
|
// then serialize each one exactly as the request does.
|
|
1699
1790
|
const isUserAuthored = summaryOptions.isUserAuthored ?? ((message: AgentMessage) => message.role === "user");
|
|
1700
|
-
const userMessages = convertToLlm(
|
|
1701
|
-
[...messagesToSummarize, ...turnPrefixMessages, ...recentMessages].filter(isUserAuthored),
|
|
1702
|
-
);
|
|
1703
|
-
const retainedUserItems: unknown[] = [...(previousReplacementHistory ?? [])];
|
|
1704
1791
|
const remoteSystemPrompt = summaryOptions.remoteSystemPrompt ?? [SUMMARIZATION_SYSTEM_PROMPT];
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
throw new Error("Stored Codex V2 compaction history contains an invalid input item");
|
|
1712
|
-
}
|
|
1713
|
-
previousCodexInput.push(item);
|
|
1714
|
-
}
|
|
1715
|
-
codexBody = await buildTransformedCodexRequestBody(
|
|
1716
|
-
model,
|
|
1717
|
-
{ systemPrompt: remoteSystemPrompt, messages, tools: summaryOptions.tools },
|
|
1718
|
-
{
|
|
1719
|
-
reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
|
|
1720
|
-
forceReasoningOff: summaryOptions.thinkingLevel === ThinkingLevel.Off,
|
|
1721
|
-
responsesLite: model.useResponsesLite,
|
|
1722
|
-
sessionId: summaryOptions.sessionId,
|
|
1723
|
-
promptCacheKey: summaryOptions.promptCacheKey,
|
|
1724
|
-
providerSessionState: summaryOptions.providerSessionState,
|
|
1725
|
-
codexCompaction: createOpenAICodexCompactionRequestContext({
|
|
1726
|
-
context: summaryOptions.codexCompaction,
|
|
1727
|
-
implementation: "responses_compaction_v2",
|
|
1728
|
-
}),
|
|
1729
|
-
},
|
|
1730
|
-
undefined,
|
|
1731
|
-
previousCodexInput,
|
|
1792
|
+
const buildV2Input = async (includeTail: boolean): Promise<RemoteCompactionV2Input> => {
|
|
1793
|
+
const messages = convertToLlm(includeTail ? remoteMessages : remotePrefixMessages);
|
|
1794
|
+
const userMessages = convertToLlm(
|
|
1795
|
+
[...messagesToSummarize, ...turnPrefixMessages, ...(includeTail ? recentMessages : [])].filter(
|
|
1796
|
+
isUserAuthored,
|
|
1797
|
+
),
|
|
1732
1798
|
);
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1799
|
+
const retainedUserItems: unknown[] = [...(previousReplacementHistory ?? [])];
|
|
1800
|
+
let codexBody: OpenAICodexCompactionBody | undefined;
|
|
1801
|
+
let remoteHistory: Array<Record<string, unknown>>;
|
|
1802
|
+
if (isCodexResponsesModel(model)) {
|
|
1803
|
+
const previousCodexInput: CodexInputItem[] = [];
|
|
1804
|
+
for (const item of previousReplacementHistory ?? []) {
|
|
1805
|
+
if (!isCodexInputItem(item)) {
|
|
1806
|
+
throw new Error("Stored Codex V2 compaction history contains an invalid input item");
|
|
1807
|
+
}
|
|
1808
|
+
previousCodexInput.push(item);
|
|
1738
1809
|
}
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
remoteHistory = stripOpenAIResponsesOutputOnlyStatusesForReplay(nativeInput);
|
|
1742
|
-
codexBody.input = remoteHistory;
|
|
1743
|
-
for (const message of userMessages) {
|
|
1744
|
-
const userBody = await buildTransformedCodexRequestBody(
|
|
1810
|
+
codexBody = await buildTransformedCodexRequestBody(
|
|
1745
1811
|
model,
|
|
1746
|
-
{ messages:
|
|
1747
|
-
{
|
|
1812
|
+
{ systemPrompt: remoteSystemPrompt, messages, tools: summaryOptions.tools },
|
|
1813
|
+
{
|
|
1814
|
+
reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
|
|
1815
|
+
forceReasoningOff: summaryOptions.thinkingLevel === ThinkingLevel.Off,
|
|
1816
|
+
responsesLite: model.useResponsesLite,
|
|
1817
|
+
sessionId: summaryOptions.sessionId,
|
|
1818
|
+
promptCacheKey: summaryOptions.promptCacheKey,
|
|
1819
|
+
providerSessionState: summaryOptions.providerSessionState,
|
|
1820
|
+
codexCompaction: createOpenAICodexCompactionRequestContext({
|
|
1821
|
+
context: summaryOptions.codexCompaction,
|
|
1822
|
+
implementation: "responses_compaction_v2",
|
|
1823
|
+
}),
|
|
1824
|
+
},
|
|
1825
|
+
undefined,
|
|
1826
|
+
previousCodexInput,
|
|
1748
1827
|
);
|
|
1749
|
-
|
|
1828
|
+
const input = Array.isArray(codexBody.input) ? codexBody.input : [];
|
|
1829
|
+
const nativeInput: Array<Record<string, unknown>> = [];
|
|
1830
|
+
for (const item of input) {
|
|
1831
|
+
if (!isRecord(item)) {
|
|
1832
|
+
throw new Error("Codex V2 compaction input contains a non-object item");
|
|
1833
|
+
}
|
|
1834
|
+
nativeInput.push(item);
|
|
1835
|
+
}
|
|
1836
|
+
remoteHistory = stripOpenAIResponsesOutputOnlyStatusesForReplay(nativeInput);
|
|
1837
|
+
codexBody.input = remoteHistory;
|
|
1838
|
+
for (const message of userMessages) {
|
|
1839
|
+
const userBody = await buildTransformedCodexRequestBody(
|
|
1840
|
+
model,
|
|
1841
|
+
{ messages: [message] },
|
|
1842
|
+
{ responsesLite: model.useResponsesLite },
|
|
1843
|
+
);
|
|
1844
|
+
retainedUserItems.push(...(userBody.input ?? []));
|
|
1845
|
+
}
|
|
1846
|
+
} else {
|
|
1847
|
+
remoteHistory = buildOpenAiResponsesCompactionInput(messages, model, previousReplacementHistory);
|
|
1848
|
+
for (const message of userMessages) {
|
|
1849
|
+
retainedUserItems.push(...buildOpenAiResponsesCompactionInput([message], model, undefined));
|
|
1850
|
+
}
|
|
1750
1851
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1852
|
+
return { codexBody, remoteHistory, retainedUserItems };
|
|
1853
|
+
};
|
|
1854
|
+
const sendV2 = async (
|
|
1855
|
+
{ codexBody, remoteHistory, retainedUserItems }: RemoteCompactionV2Input,
|
|
1856
|
+
rewriteOutputs: boolean,
|
|
1857
|
+
): Promise<void> => {
|
|
1858
|
+
const instructions = codexBody
|
|
1859
|
+
? typeof codexBody.instructions === "string"
|
|
1860
|
+
? codexBody.instructions
|
|
1861
|
+
: ""
|
|
1862
|
+
: remoteSystemPrompt.join("\n\n");
|
|
1863
|
+
const tools = codexBody
|
|
1864
|
+
? Array.isArray(codexBody.tools)
|
|
1865
|
+
? codexBody.tools
|
|
1866
|
+
: undefined
|
|
1867
|
+
: summaryOptions.tools
|
|
1868
|
+
? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
|
|
1869
|
+
: undefined;
|
|
1870
|
+
const trimmed = trimRemoteCompactionInputToContextWindow(
|
|
1871
|
+
remoteHistory,
|
|
1872
|
+
new Tokenizer(model),
|
|
1873
|
+
model.contextWindow,
|
|
1874
|
+
instructions,
|
|
1875
|
+
tools,
|
|
1876
|
+
{ rewriteOutputs },
|
|
1877
|
+
);
|
|
1878
|
+
if (trimmed.rewrittenOutputs > 0) {
|
|
1879
|
+
logger.info("Rewrote trailing tool outputs before OpenAI V2 remote compaction", {
|
|
1880
|
+
model: model.id,
|
|
1881
|
+
provider: model.provider,
|
|
1882
|
+
rewrittenOutputs: trimmed.rewrittenOutputs,
|
|
1883
|
+
estimatedTokensBefore: trimmed.estimatedTokensBefore,
|
|
1884
|
+
estimatedTokensAfter: trimmed.estimatedTokensAfter,
|
|
1885
|
+
contextWindow: model.contextWindow,
|
|
1886
|
+
});
|
|
1755
1887
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
|
|
1770
|
-
: undefined;
|
|
1771
|
-
const trimmed = trimRemoteCompactionInputToContextWindow(
|
|
1772
|
-
remoteHistory,
|
|
1773
|
-
new Tokenizer(model),
|
|
1774
|
-
model.contextWindow,
|
|
1775
|
-
instructions,
|
|
1776
|
-
tools,
|
|
1777
|
-
);
|
|
1778
|
-
if (trimmed.rewrittenOutputs > 0) {
|
|
1779
|
-
logger.info("Rewrote trailing tool outputs before OpenAI V2 remote compaction", {
|
|
1780
|
-
model: model.id,
|
|
1781
|
-
provider: model.provider,
|
|
1782
|
-
rewrittenOutputs: trimmed.rewrittenOutputs,
|
|
1783
|
-
estimatedTokensBefore: trimmed.estimatedTokensBefore,
|
|
1784
|
-
estimatedTokensAfter: trimmed.estimatedTokensAfter,
|
|
1785
|
-
contextWindow: model.contextWindow,
|
|
1888
|
+
assertRemoteCompactionInputFits(trimmed, model);
|
|
1889
|
+
const requestOptions = {
|
|
1890
|
+
sessionId: summaryOptions.sessionId,
|
|
1891
|
+
promptCacheKey: summaryOptions.promptCacheKey,
|
|
1892
|
+
retainedMessageBudget: settings.v2RetainedMessageBudget,
|
|
1893
|
+
retainedUserItems,
|
|
1894
|
+
};
|
|
1895
|
+
const request = codexBody
|
|
1896
|
+
? buildCompactionV2RequestFromBody(model, { ...codexBody, input: trimmed.input }, requestOptions)
|
|
1897
|
+
: buildCompactionV2Request(model, trimmed.input, instructions, {
|
|
1898
|
+
...requestOptions,
|
|
1899
|
+
tools,
|
|
1900
|
+
reasoning: buildCompactionV2Reasoning(model, summaryOptions.thinkingLevel),
|
|
1786
1901
|
});
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
{ signal },
|
|
1902
|
+
const remote = await withAuth(
|
|
1903
|
+
apiKey,
|
|
1904
|
+
key =>
|
|
1905
|
+
requestCompactionV2Streaming(model, key, request, signal, {
|
|
1906
|
+
fetch: summaryOptions.fetch,
|
|
1907
|
+
providerSessionState: summaryOptions.providerSessionState,
|
|
1908
|
+
preferWebsockets: summaryOptions.preferWebsockets,
|
|
1909
|
+
codexCompaction: summaryOptions.codexCompaction,
|
|
1910
|
+
}),
|
|
1911
|
+
{ signal },
|
|
1912
|
+
);
|
|
1913
|
+
preserveData = { ...preserveData, ...storeCompactionV2PreserveData(remote, model) };
|
|
1914
|
+
};
|
|
1915
|
+
const whole = await buildV2Input(true);
|
|
1916
|
+
if (whole.remoteHistory.length > 0) {
|
|
1917
|
+
try {
|
|
1918
|
+
await sendRemoteCompaction(
|
|
1919
|
+
() => sendV2(whole, !canCompactBeforeTail),
|
|
1920
|
+
async () => {
|
|
1921
|
+
const prefix = await buildV2Input(false);
|
|
1922
|
+
if (prefix.remoteHistory.length === 0) return false;
|
|
1923
|
+
await sendV2(prefix, true);
|
|
1924
|
+
return true;
|
|
1925
|
+
},
|
|
1812
1926
|
);
|
|
1813
|
-
preserveData = { ...preserveData, ...storeCompactionV2PreserveData(remote, model) };
|
|
1814
1927
|
usedRemoteCompaction = true;
|
|
1815
1928
|
} catch (err) {
|
|
1816
1929
|
// A user/session abort is a cancellation, not a remote failure —
|
|
@@ -1842,33 +1955,52 @@ export async function compact(
|
|
|
1842
1955
|
: previousV2Compaction?.provider === model.provider
|
|
1843
1956
|
? previousV2Compaction.replacementHistory
|
|
1844
1957
|
: undefined;
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1958
|
+
const convertToLlm = summaryOptions.convertToLlm ?? defaultConvertToLlm;
|
|
1959
|
+
const supportsOriginalDetail = openAiCompatSupportsImageDetailOriginal(model);
|
|
1960
|
+
const sendV1 = async (remoteHistory: Array<Record<string, unknown>>, rewriteOutputs: boolean): Promise<void> => {
|
|
1961
|
+
const remote = await withAuth(
|
|
1962
|
+
apiKey,
|
|
1963
|
+
key =>
|
|
1964
|
+
requestOpenAiRemoteCompaction(
|
|
1965
|
+
model,
|
|
1966
|
+
key,
|
|
1967
|
+
remoteHistory,
|
|
1968
|
+
summaryOptions.remoteSystemPrompt?.join("\n\n") ?? SUMMARIZATION_SYSTEM_PROMPT,
|
|
1969
|
+
signal,
|
|
1970
|
+
{
|
|
1971
|
+
fetch: summaryOptions.fetch,
|
|
1972
|
+
sessionId: summaryOptions.sessionId,
|
|
1973
|
+
providerSessionState: summaryOptions.providerSessionState,
|
|
1974
|
+
codexCompaction: summaryOptions.codexCompaction,
|
|
1975
|
+
rewriteOutputs,
|
|
1976
|
+
},
|
|
1977
|
+
),
|
|
1978
|
+
{ signal },
|
|
1979
|
+
);
|
|
1980
|
+
preserveData = withOpenAiRemoteCompactionPreserveData(preserveData, remote);
|
|
1981
|
+
};
|
|
1982
|
+
const whole = buildOpenAiNativeHistory(
|
|
1983
|
+
convertToLlm(remoteMessages),
|
|
1847
1984
|
model,
|
|
1848
1985
|
previousReplacementHistory,
|
|
1849
|
-
|
|
1986
|
+
supportsOriginalDetail,
|
|
1850
1987
|
);
|
|
1851
|
-
if (
|
|
1988
|
+
if (whole.length > 0) {
|
|
1852
1989
|
try {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1990
|
+
await sendRemoteCompaction(
|
|
1991
|
+
() => sendV1(whole, !canCompactBeforeTail),
|
|
1992
|
+
async () => {
|
|
1993
|
+
const prefix = buildOpenAiNativeHistory(
|
|
1994
|
+
convertToLlm(remotePrefixMessages),
|
|
1857
1995
|
model,
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
providerSessionState: summaryOptions.providerSessionState,
|
|
1866
|
-
codexCompaction: summaryOptions.codexCompaction,
|
|
1867
|
-
},
|
|
1868
|
-
),
|
|
1869
|
-
{ signal },
|
|
1996
|
+
previousReplacementHistory,
|
|
1997
|
+
supportsOriginalDetail,
|
|
1998
|
+
);
|
|
1999
|
+
if (prefix.length === 0) return false;
|
|
2000
|
+
await sendV1(prefix, true);
|
|
2001
|
+
return true;
|
|
2002
|
+
},
|
|
1870
2003
|
);
|
|
1871
|
-
preserveData = withOpenAiRemoteCompactionPreserveData(preserveData, remote);
|
|
1872
2004
|
usedRemoteCompaction = true;
|
|
1873
2005
|
} catch (err) {
|
|
1874
2006
|
// A user/session abort is a cancellation, not a remote failure —
|
|
@@ -1981,7 +2113,7 @@ export async function compact(
|
|
|
1981
2113
|
// history when the host can build them: kept thinking remains valid
|
|
1982
2114
|
// only when the summarized prefix matches what was sent byte for byte.
|
|
1983
2115
|
const context = summaryOptions.buildProviderContext
|
|
1984
|
-
? await summaryOptions.buildProviderContext(summarizedMessages, allMessages.slice(safeCut), signal)
|
|
2116
|
+
? await summaryOptions.buildProviderContext(summarizedMessages, allMessages.slice(safeCut), signal, model)
|
|
1985
2117
|
: {
|
|
1986
2118
|
systemPrompt: summaryOptions.remoteSystemPrompt ?? [],
|
|
1987
2119
|
messages: (summaryOptions.convertToLlm ?? defaultConvertToLlm)(summarizedMessages),
|
|
@@ -2140,6 +2272,7 @@ export async function compact(
|
|
|
2140
2272
|
tokensBefore,
|
|
2141
2273
|
details: { readFiles, modifiedFiles } as CompactionDetails,
|
|
2142
2274
|
preserveData: finalPreserveData,
|
|
2275
|
+
...(remoteReplayThroughEntryId ? { providerReplayThroughEntryId: remoteReplayThroughEntryId } : {}),
|
|
2143
2276
|
};
|
|
2144
2277
|
}
|
|
2145
2278
|
|
|
@@ -2157,7 +2290,11 @@ async function generateTurnPrefixSummary(
|
|
|
2157
2290
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), MAX_SUMMARY_TOKENS); // Smaller budget for turn prefix
|
|
2158
2291
|
|
|
2159
2292
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
2160
|
-
const conversationText = serializeConversationForSummary(
|
|
2293
|
+
const conversationText = serializeConversationForSummary(
|
|
2294
|
+
llmMessages,
|
|
2295
|
+
preferredDialect(model.id),
|
|
2296
|
+
resolveCatalogPolicy(model).classifierCotExtraction === true,
|
|
2297
|
+
);
|
|
2161
2298
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
2162
2299
|
const summarizationMessages = [
|
|
2163
2300
|
{
|