@linxiraos/pi-agent-core 1.1.13 → 1.1.15
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 +2 -47
- package/dist/types/agent.d.ts +13 -3
- package/dist/types/compaction/anthropic.d.ts +108 -0
- package/dist/types/compaction/compaction-v2-streaming.d.ts +2 -2
- package/dist/types/compaction/compaction.d.ts +7 -1
- package/dist/types/compaction/index.d.ts +1 -0
- package/dist/types/compaction/openai.d.ts +7 -7
- package/dist/types/index.d.ts +1 -0
- package/dist/types/speculative-execution.d.ts +63 -0
- package/dist/types/types.d.ts +203 -2
- package/package.json +8 -8
- package/src/agent-loop.ts +455 -76
- package/src/agent.ts +41 -21
- package/src/compaction/anthropic.ts +310 -0
- package/src/compaction/compaction-v2-streaming.ts +2 -2
- package/src/compaction/compaction.ts +204 -68
- package/src/compaction/index.ts +1 -0
- package/src/compaction/openai.ts +7 -7
- package/src/compaction/prompts/anthropic-compaction-instructions.md +17 -0
- package/src/index.ts +2 -0
- package/src/proxy.ts +1 -5
- package/src/speculative-execution.ts +953 -0
- package/src/tokenizer.ts +10 -2
- package/src/types.ts +218 -3
|
@@ -42,6 +42,15 @@ import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
|
42
42
|
import { ThinkingLevel } from "../thinking";
|
|
43
43
|
import { Tokenizer } from "../tokenizer";
|
|
44
44
|
import type { AgentMessage } from "../types";
|
|
45
|
+
import {
|
|
46
|
+
ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS,
|
|
47
|
+
buildAnthropicCompactionInstructions,
|
|
48
|
+
describeRetainedTail,
|
|
49
|
+
getPreservedAnthropicCompactionData,
|
|
50
|
+
requestAnthropicNativeCompaction,
|
|
51
|
+
shouldUseAnthropicNativeCompaction,
|
|
52
|
+
withAnthropicCompactionPreserveData,
|
|
53
|
+
} from "./anthropic";
|
|
45
54
|
import {
|
|
46
55
|
buildCompactionV2Request,
|
|
47
56
|
buildCompactionV2RequestFromBody,
|
|
@@ -53,7 +62,13 @@ import {
|
|
|
53
62
|
} from "./compaction-v2-streaming";
|
|
54
63
|
import type { CompactionEntry, SessionEntry } from "./entries";
|
|
55
64
|
import { NativeCompactionError } from "./errors";
|
|
56
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
type ConvertToLlm,
|
|
67
|
+
createBranchSummaryMessage,
|
|
68
|
+
createCompactionSummaryMessage,
|
|
69
|
+
createCustomMessage,
|
|
70
|
+
defaultConvertToLlm,
|
|
71
|
+
} from "./messages";
|
|
57
72
|
import {
|
|
58
73
|
buildOpenAiNativeHistory,
|
|
59
74
|
getPreservedOpenAiRemoteCompactionData,
|
|
@@ -224,7 +239,11 @@ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
|
|
224
239
|
v2RetainedMessageBudget: V2_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
225
240
|
};
|
|
226
241
|
|
|
227
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Whether a compaction candidate preserves provider-native transport under the
|
|
244
|
+
* effective settings: an OpenAI Responses compact route (V1 or streamed V2) or
|
|
245
|
+
* the Anthropic compaction beta.
|
|
246
|
+
*/
|
|
228
247
|
export function shouldUseProviderNativeCompaction(
|
|
229
248
|
model: Model,
|
|
230
249
|
settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">,
|
|
@@ -232,7 +251,8 @@ export function shouldUseProviderNativeCompaction(
|
|
|
232
251
|
if (settings.remoteEnabled === false) return false;
|
|
233
252
|
return (
|
|
234
253
|
shouldUseOpenAiRemoteCompaction(model) ||
|
|
235
|
-
(settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(model))
|
|
254
|
+
(settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(model)) ||
|
|
255
|
+
shouldUseAnthropicNativeCompaction(model)
|
|
236
256
|
);
|
|
237
257
|
}
|
|
238
258
|
|
|
@@ -395,22 +415,6 @@ export function resolveThresholdTokens(contextWindow: number, settings: Compacti
|
|
|
395
415
|
// Cut point detection
|
|
396
416
|
// ============================================================================
|
|
397
417
|
|
|
398
|
-
function estimateEntriesTokens(
|
|
399
|
-
entries: SessionEntry[],
|
|
400
|
-
tokenizer: Tokenizer,
|
|
401
|
-
startIndex: number,
|
|
402
|
-
endIndex: number,
|
|
403
|
-
): number {
|
|
404
|
-
let total = 0;
|
|
405
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
406
|
-
const msg = getMessageFromEntry(entries[i]);
|
|
407
|
-
if (msg) {
|
|
408
|
-
total += tokenizer.countMessage(msg);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return total;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
418
|
/**
|
|
415
419
|
* Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
|
|
416
420
|
* Never cut at tool results (they must follow their tool call).
|
|
@@ -1252,6 +1256,8 @@ export interface CompactionPreparation {
|
|
|
1252
1256
|
tokensBefore: number;
|
|
1253
1257
|
/** Summary from previous compaction, for iterative update */
|
|
1254
1258
|
previousSummary?: string;
|
|
1259
|
+
/** ISO timestamp of the previous compaction entry, for iterative update */
|
|
1260
|
+
previousSummaryTimestamp?: string;
|
|
1255
1261
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
1256
1262
|
previousPreserveData?: Record<string, unknown>;
|
|
1257
1263
|
/** File operations extracted from messagesToSummarize */
|
|
@@ -1331,16 +1337,10 @@ export function prepareCompaction(
|
|
|
1331
1337
|
|
|
1332
1338
|
let prevCompactionIndex = findReadableCompactionIndex(pathEntries, settings, activeModel);
|
|
1333
1339
|
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
// must not resurrect the dropped pre-clear turns into its summary — matching
|
|
1337
|
-
// how buildSessionContext starts the model-context rebuild after the boundary.
|
|
1338
|
-
// A boundary after the last reusable compaction supersedes it: the pre-reset
|
|
1339
|
-
// summary was cleared too, so drop the previous-compaction reuse and start
|
|
1340
|
-
// fresh after the boundary. A boundary at or before that compaction is already
|
|
1341
|
-
// superseded by it, so only scan newer entries.
|
|
1340
|
+
// A reset after the reusable compaction clears its summary too. An older
|
|
1341
|
+
// reset still bounds how far we may recover that compaction's kept messages.
|
|
1342
1342
|
let resetBoundaryIndex = -1;
|
|
1343
|
-
for (let i = pathEntries.length - 1; i
|
|
1343
|
+
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
|
1344
1344
|
if (pathEntries[i].type === "reset_boundary") {
|
|
1345
1345
|
resetBoundaryIndex = i;
|
|
1346
1346
|
break;
|
|
@@ -1349,14 +1349,43 @@ export function prepareCompaction(
|
|
|
1349
1349
|
if (resetBoundaryIndex > prevCompactionIndex) {
|
|
1350
1350
|
prevCompactionIndex = -1;
|
|
1351
1351
|
}
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1352
|
+
const previousCompaction =
|
|
1353
|
+
prevCompactionIndex >= 0 ? (pathEntries[prevCompactionIndex] as CompactionEntry) : undefined;
|
|
1354
|
+
let boundaryStart = Math.max(prevCompactionIndex, resetBoundaryIndex) + 1;
|
|
1355
|
+
if (
|
|
1356
|
+
previousCompaction &&
|
|
1357
|
+
!getCompactionV2PreserveData(previousCompaction.preserveData) &&
|
|
1358
|
+
!getPreservedOpenAiRemoteCompactionData(previousCompaction.preserveData)
|
|
1359
|
+
) {
|
|
1360
|
+
// Local summaries exclude the retained tail, whose original entries precede
|
|
1361
|
+
// the compaction record. Native replay already carries that tail. Only look
|
|
1362
|
+
// backwards: advisor snapshots put all retained messages after the summary
|
|
1363
|
+
// and may carry a keep ID from their previous, differently indexed snapshot.
|
|
1364
|
+
for (let i = resetBoundaryIndex + 1; i < prevCompactionIndex; i++) {
|
|
1365
|
+
if (pathEntries[i].id === previousCompaction.firstKeptEntryId) {
|
|
1366
|
+
boundaryStart = i;
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Keep original IDs beside the converted messages so estimation, cutting,
|
|
1373
|
+
// and all three output regions share one sequence without journal metadata.
|
|
1374
|
+
const compactionEntries: SessionEntry[] = [];
|
|
1375
|
+
const compactionMessages: AgentMessage[] = [];
|
|
1376
|
+
for (let i = boundaryStart; i < pathEntries.length; i++) {
|
|
1377
|
+
const entry = pathEntries[i];
|
|
1378
|
+
const message = getMessageFromEntry(entry);
|
|
1379
|
+
if (!message) continue;
|
|
1380
|
+
compactionEntries.push(entry);
|
|
1381
|
+
compactionMessages.push(message);
|
|
1382
|
+
}
|
|
1354
1383
|
|
|
1355
1384
|
const lastUsage = getLastAssistantUsage(pathEntries);
|
|
1356
1385
|
const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
|
|
1357
1386
|
let keepRecentTokens = settings.keepRecentTokens;
|
|
1358
1387
|
if (lastUsage) {
|
|
1359
|
-
const estimatedTokens =
|
|
1388
|
+
const estimatedTokens = tokenizer.countMessages(compactionMessages);
|
|
1360
1389
|
const promptTokens = calculatePromptTokens(lastUsage);
|
|
1361
1390
|
const ratio = estimatedTokens > 0 ? promptTokens / estimatedTokens : 0;
|
|
1362
1391
|
if (Number.isFinite(ratio) && ratio > 1) {
|
|
@@ -1364,10 +1393,10 @@ export function prepareCompaction(
|
|
|
1364
1393
|
}
|
|
1365
1394
|
}
|
|
1366
1395
|
|
|
1367
|
-
const cutPoint = findCutPoint(
|
|
1396
|
+
const cutPoint = findCutPoint(compactionEntries, tokenizer, 0, compactionEntries.length, keepRecentTokens);
|
|
1368
1397
|
|
|
1369
1398
|
// Get ID of first kept entry
|
|
1370
|
-
const firstKeptEntry =
|
|
1399
|
+
const firstKeptEntry = compactionEntries[cutPoint.firstKeptEntryIndex];
|
|
1371
1400
|
if (!firstKeptEntry?.id) {
|
|
1372
1401
|
return undefined; // Session needs migration
|
|
1373
1402
|
}
|
|
@@ -1375,42 +1404,16 @@ export function prepareCompaction(
|
|
|
1375
1404
|
|
|
1376
1405
|
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
|
|
1377
1406
|
|
|
1378
|
-
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// Messages for turn prefix summary (if splitting a turn)
|
|
1386
|
-
const turnPrefixMessages: AgentMessage[] = [];
|
|
1387
|
-
if (cutPoint.isSplitTurn) {
|
|
1388
|
-
for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
|
|
1389
|
-
const msg = getMessageFromEntry(pathEntries[i]);
|
|
1390
|
-
if (msg) turnPrefixMessages.push(msg);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// Messages kept after compaction (recent history)
|
|
1395
|
-
const recentMessages: AgentMessage[] = [];
|
|
1396
|
-
for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
|
|
1397
|
-
const msg = getMessageFromEntry(pathEntries[i]);
|
|
1398
|
-
if (msg) recentMessages.push(msg);
|
|
1399
|
-
}
|
|
1407
|
+
const messagesToSummarize = compactionMessages.slice(0, historyEnd);
|
|
1408
|
+
const turnPrefixMessages = cutPoint.isSplitTurn
|
|
1409
|
+
? compactionMessages.slice(cutPoint.turnStartIndex, cutPoint.firstKeptEntryIndex)
|
|
1410
|
+
: [];
|
|
1411
|
+
const recentMessages = compactionMessages.slice(cutPoint.firstKeptEntryIndex);
|
|
1400
1412
|
// Nothing to summarize means compaction would be a no-op.
|
|
1401
1413
|
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
|
|
1402
1414
|
return undefined;
|
|
1403
1415
|
}
|
|
1404
1416
|
|
|
1405
|
-
// Get previous summary and preserved data for iterative updates
|
|
1406
|
-
let previousSummary: string | undefined;
|
|
1407
|
-
let previousPreserveData: Record<string, unknown> | undefined;
|
|
1408
|
-
if (prevCompactionIndex >= 0) {
|
|
1409
|
-
const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;
|
|
1410
|
-
previousSummary = prevCompaction.summary;
|
|
1411
|
-
previousPreserveData = prevCompaction.preserveData;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1417
|
// Extract file operations from messages and previous compaction
|
|
1415
1418
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
|
1416
1419
|
|
|
@@ -1428,8 +1431,9 @@ export function prepareCompaction(
|
|
|
1428
1431
|
recentMessages,
|
|
1429
1432
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
1430
1433
|
tokensBefore,
|
|
1431
|
-
previousSummary,
|
|
1432
|
-
|
|
1434
|
+
previousSummary: previousCompaction?.summary,
|
|
1435
|
+
previousSummaryTimestamp: previousCompaction?.timestamp,
|
|
1436
|
+
previousPreserveData: previousCompaction?.preserveData,
|
|
1433
1437
|
fileOps,
|
|
1434
1438
|
settings,
|
|
1435
1439
|
};
|
|
@@ -1585,6 +1589,9 @@ export async function compact(
|
|
|
1585
1589
|
tools: options?.tools,
|
|
1586
1590
|
fetch: options?.fetch,
|
|
1587
1591
|
completeImpl: options?.completeImpl,
|
|
1592
|
+
// The caller's opt-out must reach every summarization oneshot, otherwise
|
|
1593
|
+
// an outer retry loop multiplies with the inner one (see SummaryOptions).
|
|
1594
|
+
oneshotRetry: options?.oneshotRetry,
|
|
1588
1595
|
};
|
|
1589
1596
|
|
|
1590
1597
|
const previousSnapcompactArchive = snapcompact.getPreservedArchive(previousPreserveData);
|
|
@@ -1599,7 +1606,10 @@ export async function compact(
|
|
|
1599
1606
|
? createSnapcompactArchiveMigrationMessage(previousSnapcompactArchiveText)
|
|
1600
1607
|
: undefined;
|
|
1601
1608
|
|
|
1602
|
-
let preserveData =
|
|
1609
|
+
let preserveData = withAnthropicCompactionPreserveData(
|
|
1610
|
+
withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined),
|
|
1611
|
+
undefined,
|
|
1612
|
+
);
|
|
1603
1613
|
const remoteMessages: AgentMessage[] = [
|
|
1604
1614
|
...(snapcompactArchiveMigrationMessage ? [snapcompactArchiveMigrationMessage] : []),
|
|
1605
1615
|
...messagesToSummarize,
|
|
@@ -1784,6 +1794,112 @@ export async function compact(
|
|
|
1784
1794
|
}
|
|
1785
1795
|
}
|
|
1786
1796
|
|
|
1797
|
+
// Anthropic server-side compaction: the live turn's request shape plus the
|
|
1798
|
+
// compact edit, so the API summarizes from its cached prefix. The summary is
|
|
1799
|
+
// real text, persisted both as the entry summary and as the native replay
|
|
1800
|
+
// payload. A context below the API's trigger floor cannot compact remotely
|
|
1801
|
+
// and takes the local summarizer instead — an eligibility boundary, not a
|
|
1802
|
+
// failure.
|
|
1803
|
+
let nativeSummary: string | undefined;
|
|
1804
|
+
let nativeEncryptedContent: string | undefined;
|
|
1805
|
+
let nativeUsedTokens: number | undefined;
|
|
1806
|
+
if (
|
|
1807
|
+
!usedRemoteCompaction &&
|
|
1808
|
+
settings.remoteEnabled !== false &&
|
|
1809
|
+
shouldUseAnthropicNativeCompaction(model) &&
|
|
1810
|
+
tokensBefore >= ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS
|
|
1811
|
+
) {
|
|
1812
|
+
const previousNative = getPreservedAnthropicCompactionData(previousPreserveData);
|
|
1813
|
+
// Lead with the previous summary exactly as the live context renders it:
|
|
1814
|
+
// natively when this provider wrote it, as text otherwise. The request
|
|
1815
|
+
// then shares the live turn's prefix byte-for-byte. A prior snapcompact
|
|
1816
|
+
// archive is already merged into that summary text, so the archive
|
|
1817
|
+
// migration message the OpenAI lanes carry is omitted here.
|
|
1818
|
+
// The rewrite marker must precede every message this request replays —
|
|
1819
|
+
// summarized history and retained tail alike — exactly like the live
|
|
1820
|
+
// context rebuild predates its tail. A previous compaction's commit
|
|
1821
|
+
// timestamp is newer than re-retained or re-summarized turns, so
|
|
1822
|
+
// reusing it would strip their bound thinking only in this request,
|
|
1823
|
+
// diverging from the cached live prefix (and possibly dropping below
|
|
1824
|
+
// the trigger). Manually built preparations with no input at all fall
|
|
1825
|
+
// back to the current time.
|
|
1826
|
+
const firstReplayed = messagesToSummarize[0] ?? turnPrefixMessages[0] ?? recentMessages[0];
|
|
1827
|
+
const previousSummaryAt =
|
|
1828
|
+
firstReplayed !== undefined ? new Date(firstReplayed.timestamp - 1).toISOString() : new Date().toISOString();
|
|
1829
|
+
const previousSummaryMessage = previousSummaryForCompaction
|
|
1830
|
+
? createCompactionSummaryMessage(previousSummaryForCompaction, tokensBefore, previousSummaryAt, {
|
|
1831
|
+
providerPayload:
|
|
1832
|
+
previousNative?.provider === model.provider
|
|
1833
|
+
? {
|
|
1834
|
+
type: "anthropicCompaction",
|
|
1835
|
+
provider: previousNative.provider,
|
|
1836
|
+
content: previousNative.content,
|
|
1837
|
+
...(previousNative.encryptedContent
|
|
1838
|
+
? { encryptedContent: previousNative.encryptedContent }
|
|
1839
|
+
: {}),
|
|
1840
|
+
...(previousNative.filesText ? { filesText: previousNative.filesText } : {}),
|
|
1841
|
+
}
|
|
1842
|
+
: undefined,
|
|
1843
|
+
})
|
|
1844
|
+
: undefined;
|
|
1845
|
+
const convertToLlm = summaryOptions.convertToLlm ?? defaultConvertToLlm;
|
|
1846
|
+
const retainedTail = convertToLlm(recentMessages);
|
|
1847
|
+
const messages = [
|
|
1848
|
+
...convertToLlm([
|
|
1849
|
+
...(previousSummaryMessage ? [previousSummaryMessage] : []),
|
|
1850
|
+
...messagesToSummarize,
|
|
1851
|
+
...turnPrefixMessages,
|
|
1852
|
+
]),
|
|
1853
|
+
...retainedTail,
|
|
1854
|
+
];
|
|
1855
|
+
try {
|
|
1856
|
+
const remote = await requestAnthropicNativeCompaction(
|
|
1857
|
+
model,
|
|
1858
|
+
apiKey,
|
|
1859
|
+
{
|
|
1860
|
+
systemPrompt: summaryOptions.remoteSystemPrompt ?? [SUMMARIZATION_SYSTEM_PROMPT],
|
|
1861
|
+
messages,
|
|
1862
|
+
tools: summaryOptions.tools,
|
|
1863
|
+
instructions: buildAnthropicCompactionInstructions(
|
|
1864
|
+
summaryOptions.promptOverride ?? SUMMARIZATION_PROMPT,
|
|
1865
|
+
customInstructions,
|
|
1866
|
+
formatAdditionalContext(summaryOptions.extraContext).trim() || undefined,
|
|
1867
|
+
describeRetainedTail(retainedTail),
|
|
1868
|
+
),
|
|
1869
|
+
maxTokens: Math.min(Math.floor(0.8 * reserveTokens), MAX_SUMMARY_TOKENS),
|
|
1870
|
+
reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
|
|
1871
|
+
},
|
|
1872
|
+
signal,
|
|
1873
|
+
{
|
|
1874
|
+
initiatorOverride: summaryOptions.initiatorOverride,
|
|
1875
|
+
metadata: summaryOptions.metadata,
|
|
1876
|
+
fetch: summaryOptions.fetch,
|
|
1877
|
+
sessionId: summaryOptions.sessionId,
|
|
1878
|
+
promptCacheKey: summaryOptions.promptCacheKey,
|
|
1879
|
+
providerSessionState: summaryOptions.providerSessionState,
|
|
1880
|
+
completeImpl: summaryOptions.completeImpl,
|
|
1881
|
+
telemetry: summaryOptions.telemetry,
|
|
1882
|
+
retry: summaryOneshotRetry(summaryOptions),
|
|
1883
|
+
},
|
|
1884
|
+
);
|
|
1885
|
+
nativeSummary = remote.content;
|
|
1886
|
+
nativeEncryptedContent = remote.encryptedContent;
|
|
1887
|
+
nativeUsedTokens = calculatePromptTokens(remote.usage);
|
|
1888
|
+
usedRemoteCompaction = true;
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
// A user/session abort is a cancellation, not a remote failure —
|
|
1891
|
+
// swallowing it here would downgrade Esc into "fall back to local
|
|
1892
|
+
// summarization" and keep compaction running on an aborted signal.
|
|
1893
|
+
if (signal?.aborted) throw err;
|
|
1894
|
+
nativeCompactionError = selectNativeCompactionError(nativeCompactionError, err);
|
|
1895
|
+
logger.warn("Anthropic server-side compaction failed", {
|
|
1896
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1897
|
+
model: model.id,
|
|
1898
|
+
provider: model.provider,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1787
1903
|
if (!usedRemoteCompaction && nativeCompactionError !== undefined && !summaryOptions.remoteEndpoint) {
|
|
1788
1904
|
throw new NativeCompactionError(nativeCompactionError);
|
|
1789
1905
|
}
|
|
@@ -1791,7 +1907,11 @@ export async function compact(
|
|
|
1791
1907
|
// Generate summaries (can be parallel if both needed) and merge into one
|
|
1792
1908
|
let summary: string;
|
|
1793
1909
|
|
|
1794
|
-
if (
|
|
1910
|
+
if (nativeSummary !== undefined) {
|
|
1911
|
+
// The API wrote a real summary; it is the entry text. The replayed
|
|
1912
|
+
// block below stays verbatim so it matches the opaque state.
|
|
1913
|
+
summary = nativeSummary;
|
|
1914
|
+
} else if (usedRemoteCompaction) {
|
|
1795
1915
|
// Remote compaction (V2 or V1) already compacted remotely; the durable
|
|
1796
1916
|
// history lives in the provider replay payload (preserveData). Skip local
|
|
1797
1917
|
// summarization so a successful remote compaction never pays for a second,
|
|
@@ -1850,6 +1970,22 @@ export async function compact(
|
|
|
1850
1970
|
// Compute file lists and append to summary
|
|
1851
1971
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
1852
1972
|
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
1973
|
+
if (nativeSummary !== undefined) {
|
|
1974
|
+
// The replayed block stays byte-identical to the API's summary so it
|
|
1975
|
+
// matches `encryptedContent`. The harness file lists above travel
|
|
1976
|
+
// separately: the converter replaces the summary message with the
|
|
1977
|
+
// block and skips its text, so they would otherwise be invisible to
|
|
1978
|
+
// this provider. Every other provider keeps reading the entry text.
|
|
1979
|
+
const filesText = upsertFileOperations("", readFiles, modifiedFiles, fileOps.read) || undefined;
|
|
1980
|
+
preserveData = withAnthropicCompactionPreserveData(preserveData, {
|
|
1981
|
+
provider: model.provider,
|
|
1982
|
+
content: nativeSummary,
|
|
1983
|
+
...(nativeEncryptedContent ? { encryptedContent: nativeEncryptedContent } : {}),
|
|
1984
|
+
...(filesText ? { filesText } : {}),
|
|
1985
|
+
model: model.id,
|
|
1986
|
+
usedTokens: nativeUsedTokens,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1853
1989
|
|
|
1854
1990
|
if (!firstKeptEntryId) {
|
|
1855
1991
|
throw new Error("First kept entry has no ID - session may need migration");
|
package/src/compaction/index.ts
CHANGED
package/src/compaction/openai.ts
CHANGED
|
@@ -66,14 +66,14 @@ export * from "./compaction-v2-streaming";
|
|
|
66
66
|
export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Hard ceiling on remote compaction HTTP requests. Unlike every
|
|
70
|
-
* stream (guarded by first-event/idle watchdogs in pi-ai), these are
|
|
71
|
-
* fetches awaiting one non-streamed JSON body — a connection silently
|
|
72
|
-
* by a middlebox would otherwise hang the whole compaction pipeline
|
|
73
|
-
* (frozen "Auto context-full maintenance…" spinner, manual /compact
|
|
74
|
-
* behind it). On timeout the caller falls back to local summarization.
|
|
69
|
+
* Hard ceiling on remote compaction HTTP requests (5 minutes). Unlike every
|
|
70
|
+
* provider stream (guarded by first-event/idle watchdogs in pi-ai), these are
|
|
71
|
+
* raw fetches awaiting one non-streamed JSON body — a connection silently
|
|
72
|
+
* dropped by a middlebox would otherwise hang the whole compaction pipeline
|
|
73
|
+
* forever (frozen "Auto context-full maintenance…" spinner, manual /compact
|
|
74
|
+
* queueing behind it). On timeout the caller falls back to local summarization.
|
|
75
75
|
*/
|
|
76
|
-
export const REMOTE_COMPACTION_TIMEOUT_MS =
|
|
76
|
+
export const REMOTE_COMPACTION_TIMEOUT_MS = 300_000;
|
|
77
77
|
|
|
78
78
|
const DEFAULT_AZURE_API_VERSION = "v1";
|
|
79
79
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{{#if retainedTail}}
|
|
2
|
+
SCOPE: The conversation's final {{#when retainedTail.count "==" 1}}{{retainedTail.role}} message stays{{else}}{{retainedTail.count}} messages, starting with a {{retainedTail.role}} message, stay{{/when}} in context verbatim after your summary. Summarize ONLY the history before those messages. You MUST NOT restate anything from those final messages — the reader sees them right after the summary — and you MUST treat them as the most recent state when describing progress and next steps.
|
|
3
|
+
{{else}}
|
|
4
|
+
SCOPE: The conversation above is the transcript to summarize. The API replaces everything before your summary with it, so nothing you leave out survives into the next context window.
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
{{#if extraContext}}
|
|
8
|
+
{{extraContext}}
|
|
9
|
+
|
|
10
|
+
{{/if}}
|
|
11
|
+
{{basePrompt}}
|
|
12
|
+
{{#if customInstructions}}
|
|
13
|
+
|
|
14
|
+
Additional focus: {{customInstructions}}
|
|
15
|
+
{{/if}}
|
|
16
|
+
|
|
17
|
+
You MUST NOT call any tools while writing the summary; respond with the summary text only.
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from "./proxy";
|
|
|
14
14
|
export * from "./replay-policy";
|
|
15
15
|
// Run-level telemetry collector + aggregators
|
|
16
16
|
export * from "./run-collector";
|
|
17
|
+
// Speculative execution coordinator
|
|
18
|
+
export * from "./speculative-execution";
|
|
17
19
|
// Telemetry
|
|
18
20
|
export * from "./telemetry";
|
|
19
21
|
// Thinking selectors
|
package/src/proxy.ts
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
type StreamingPartialJsonCarrier,
|
|
21
21
|
setStreamingPartialJson,
|
|
22
22
|
} from "@linxiraos/pi-ai/utils/block-symbols";
|
|
23
|
-
import { calculateCost } from "@linxiraos/pi-catalog/models";
|
|
24
23
|
import { parseStreamingJson, readSseJson } from "@linxiraos/pi-utils";
|
|
25
24
|
|
|
26
25
|
// Event stream adapter for proxy SSE events
|
|
@@ -171,7 +170,7 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
171
170
|
response.body as ReadableStream<Uint8Array>,
|
|
172
171
|
options.signal,
|
|
173
172
|
)) {
|
|
174
|
-
const parsedEvent = processProxyEvent(
|
|
173
|
+
const parsedEvent = processProxyEvent(event, partial, partialJsonByIndex);
|
|
175
174
|
if (parsedEvent) {
|
|
176
175
|
if (parsedEvent.type === "done" || parsedEvent.type === "error") {
|
|
177
176
|
sawTerminalEvent = true;
|
|
@@ -233,7 +232,6 @@ function scrubPartialJson(partial: AssistantMessage): void {
|
|
|
233
232
|
* reads as still-streaming.
|
|
234
233
|
*/
|
|
235
234
|
function processProxyEvent(
|
|
236
|
-
model: Model,
|
|
237
235
|
proxyEvent: ProxyAssistantMessageEvent,
|
|
238
236
|
partial: AssistantMessage,
|
|
239
237
|
partialJsonByIndex: Map<number, string>,
|
|
@@ -375,7 +373,6 @@ function processProxyEvent(
|
|
|
375
373
|
partial.stopReason = proxyEvent.reason;
|
|
376
374
|
partial.usage = proxyEvent.usage;
|
|
377
375
|
if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
|
|
378
|
-
calculateCost(model, partial.usage);
|
|
379
376
|
scrubPartialJson(partial);
|
|
380
377
|
return { type: "done", reason: proxyEvent.reason, message: partial };
|
|
381
378
|
|
|
@@ -384,7 +381,6 @@ function processProxyEvent(
|
|
|
384
381
|
partial.errorMessage = proxyEvent.errorMessage;
|
|
385
382
|
partial.usage = proxyEvent.usage;
|
|
386
383
|
if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
|
|
387
|
-
calculateCost(model, partial.usage);
|
|
388
384
|
scrubPartialJson(partial);
|
|
389
385
|
return { type: "error", reason: proxyEvent.reason, error: partial };
|
|
390
386
|
}
|