@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.
@@ -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 { type ConvertToLlm, createBranchSummaryMessage, createCustomMessage, defaultConvertToLlm } from "./messages";
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
- /** Whether a compaction candidate preserves provider-native transport under the effective settings. */
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
- // Honor the latest `/clear` reset boundary. `/clear` records a
1335
- // `reset_boundary` marker and reports the model context empty, so compaction
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 > prevCompactionIndex; 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 boundaryStart = Math.max(prevCompactionIndex, resetBoundaryIndex) + 1;
1353
- const boundaryEnd = pathEntries.length;
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 = estimateEntriesTokens(pathEntries, tokenizer, boundaryStart, boundaryEnd);
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(pathEntries, tokenizer, boundaryStart, boundaryEnd, keepRecentTokens);
1396
+ const cutPoint = findCutPoint(compactionEntries, tokenizer, 0, compactionEntries.length, keepRecentTokens);
1368
1397
 
1369
1398
  // Get ID of first kept entry
1370
- const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
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
- // Messages to summarize (will be discarded after summary)
1379
- const messagesToSummarize: AgentMessage[] = [];
1380
- for (let i = boundaryStart; i < historyEnd; i++) {
1381
- const msg = getMessageFromEntry(pathEntries[i]);
1382
- if (msg) messagesToSummarize.push(msg);
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
- previousPreserveData,
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 = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
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 (usedRemoteCompaction) {
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");
@@ -2,6 +2,7 @@
2
2
  * Compaction and summarization utilities.
3
3
  */
4
4
 
5
+ export * from "./anthropic";
5
6
  export * from "./branch-summarization";
6
7
  export * from "./compaction";
7
8
  export * from "./entries";
@@ -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 provider
70
- * stream (guarded by first-event/idle watchdogs in pi-ai), these are raw
71
- * fetches awaiting one non-streamed JSON body — a connection silently dropped
72
- * by a middlebox would otherwise hang the whole compaction pipeline forever
73
- * (frozen "Auto context-full maintenance…" spinner, manual /compact queueing
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 = 180_000;
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(model, event, partial, partialJsonByIndex);
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
  }