@oh-my-pi/pi-agent-core 17.2.0 → 17.2.2

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.2] - 2026-07-31
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where response-only usage records were incorrectly treated as authoritative context anchors, while ensuring prompt and total-only provider telemetry remains preserved.
10
+ - Fixed context compaction summaries growing excessively with large context windows by capping the summary output budget to 16,384 tokens, ensuring conversations are properly compressed rather than duplicated.
11
+
5
12
  ## [17.2.0] - 2026-07-30
6
13
 
7
14
  ### Fixed
@@ -52,18 +52,30 @@ export interface CompactionSettings {
52
52
  }
53
53
  /** Reserve applied when {@link CompactionSettings.reserveTokens} is unset. */
54
54
  export declare const DEFAULT_RESERVE_TOKENS = 16384;
55
+ /**
56
+ * Hard ceiling on a generated compaction summary.
57
+ *
58
+ * The summary budget is `floor(0.8 * reserveTokens)`, and the effective reserve is
59
+ * at least 15% of the declared context window, so a 1M-token window authorizes a
60
+ * ~120k-token summary. At that size the model copies rather than compresses, and
61
+ * output is the slowest and most expensive token class. Capping absolutely keeps
62
+ * the compression ratio improving with window size instead of degrading. The value
63
+ * mirrors {@link DEFAULT_RESERVE_TOKENS} so this adds no new tuning constant.
64
+ */
65
+ export declare const MAX_SUMMARY_TOKENS = 16384;
55
66
  export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
56
67
  /** Whether a compaction candidate preserves provider-native transport under the effective settings. */
57
68
  export declare function shouldUseProviderNativeCompaction(model: Model, settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">): boolean;
58
69
  /**
59
70
  * Calculate total context tokens from usage.
60
- * Uses the native totalTokens field when available, falls back to computing from components.
61
- * Provider-side orchestration tokens are billable but never replay into the
62
- * conversation prefix, so they are excluded from context sizing to keep
63
- * auto-compaction and context-promotion thresholds honest.
71
+ * Prefers an explicit provider-reported context occupancy when available.
72
+ * Otherwise uses totalTokens and falls back to computing from billable
73
+ * components. Provider-side orchestration tokens are billable but never replay
74
+ * into the conversation prefix, so they are excluded from context sizing.
64
75
  */
65
76
  export declare function calculateContextTokens(usage: Usage): number;
66
77
  export declare function calculatePromptTokens(usage: Usage): number;
78
+ export declare function hasContextTokenUsage(usage: Usage): boolean;
67
79
  /**
68
80
  * Find the last non-aborted assistant message usage from session entries.
69
81
  */
@@ -65,6 +65,7 @@ export interface OpenAiRemoteCompactionResponse extends OpenAiRemoteCompactionPr
65
65
  export interface RemoteCompactionRequest {
66
66
  systemPrompt: string;
67
67
  prompt: string;
68
+ maxTokens?: number;
68
69
  }
69
70
  export interface RemoteCompactionResponse {
70
71
  summary: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "17.2.0",
4
+ "version": "17.2.2",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,12 +35,12 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "17.2.0",
39
- "@oh-my-pi/pi-catalog": "17.2.0",
40
- "@oh-my-pi/pi-natives": "17.2.0",
41
- "@oh-my-pi/pi-utils": "17.2.0",
42
- "@oh-my-pi/pi-wire": "17.2.0",
43
- "@oh-my-pi/snapcompact": "17.2.0",
38
+ "@oh-my-pi/pi-ai": "17.2.2",
39
+ "@oh-my-pi/pi-catalog": "17.2.2",
40
+ "@oh-my-pi/pi-natives": "17.2.2",
41
+ "@oh-my-pi/pi-utils": "17.2.2",
42
+ "@oh-my-pi/pi-wire": "17.2.2",
43
+ "@oh-my-pi/snapcompact": "17.2.2",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
@@ -187,6 +187,18 @@ export interface CompactionSettings {
187
187
  /** Reserve applied when {@link CompactionSettings.reserveTokens} is unset. */
188
188
  export const DEFAULT_RESERVE_TOKENS = 16384;
189
189
 
190
+ /**
191
+ * Hard ceiling on a generated compaction summary.
192
+ *
193
+ * The summary budget is `floor(0.8 * reserveTokens)`, and the effective reserve is
194
+ * at least 15% of the declared context window, so a 1M-token window authorizes a
195
+ * ~120k-token summary. At that size the model copies rather than compresses, and
196
+ * output is the slowest and most expensive token class. Capping absolutely keeps
197
+ * the compression ratio improving with window size instead of degrading. The value
198
+ * mirrors {@link DEFAULT_RESERVE_TOKENS} so this adds no new tuning constant.
199
+ */
200
+ export const MAX_SUMMARY_TOKENS = DEFAULT_RESERVE_TOKENS;
201
+
190
202
  // reserveTokens is deliberately absent: an unset reserve is what marks it as
191
203
  // defaulted, which resolveBudgetReserveTokens needs to distinguish "user never
192
204
  // chose a reserve" from "user explicitly configured the default value".
@@ -221,12 +233,15 @@ export function shouldUseProviderNativeCompaction(
221
233
 
222
234
  /**
223
235
  * Calculate total context tokens from usage.
224
- * Uses the native totalTokens field when available, falls back to computing from components.
225
- * Provider-side orchestration tokens are billable but never replay into the
226
- * conversation prefix, so they are excluded from context sizing to keep
227
- * auto-compaction and context-promotion thresholds honest.
236
+ * Prefers an explicit provider-reported context occupancy when available.
237
+ * Otherwise uses totalTokens and falls back to computing from billable
238
+ * components. Provider-side orchestration tokens are billable but never replay
239
+ * into the conversation prefix, so they are excluded from context sizing.
228
240
  */
229
241
  export function calculateContextTokens(usage: Usage): number {
242
+ if (usage.contextTokens !== undefined) {
243
+ return Math.max(0, usage.contextTokens);
244
+ }
230
245
  const orchestration = usage.orchestration;
231
246
  const orchestrationTotal = orchestration
232
247
  ? (orchestration.input ?? 0) + (orchestration.output ?? 0) + (orchestration.cacheRead ?? 0)
@@ -236,6 +251,9 @@ export function calculateContextTokens(usage: Usage): number {
236
251
  }
237
252
 
238
253
  export function calculatePromptTokens(usage: Usage): number {
254
+ if (usage.contextTokens !== undefined) {
255
+ return Math.max(0, usage.contextTokens);
256
+ }
239
257
  const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
240
258
  if (promptTokens > 0) {
241
259
  return promptTokens;
@@ -243,6 +261,14 @@ export function calculatePromptTokens(usage: Usage): number {
243
261
  return calculateContextTokens(usage);
244
262
  }
245
263
 
264
+ export function hasContextTokenUsage(usage: Usage): boolean {
265
+ return (
266
+ (usage.contextTokens ?? 0) > 0 ||
267
+ usage.input + usage.cacheRead + usage.cacheWrite > 0 ||
268
+ calculateContextTokens(usage) > usage.output
269
+ );
270
+ }
271
+
246
272
  /**
247
273
  * Get usage from an assistant message if available.
248
274
  * Skips aborted and error messages as they don't have valid usage data.
@@ -842,7 +868,7 @@ export async function generateSummary(
842
868
  previousSummary?: string,
843
869
  options?: SummaryOptions,
844
870
  ): Promise<string> {
845
- const maxTokens = Math.floor(0.8 * reserveTokens);
871
+ const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), MAX_SUMMARY_TOKENS);
846
872
 
847
873
  // Use update prompt if we have a previous summary, otherwise initial prompt
848
874
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
@@ -881,7 +907,7 @@ export async function generateSummary(
881
907
  key =>
882
908
  requestRemoteCompaction(
883
909
  endpoint,
884
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText },
910
+ { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
885
911
  signal,
886
912
  { fetch: options.fetch, model, apiKey: key },
887
913
  ),
@@ -1086,7 +1112,7 @@ async function generateShortSummary(
1086
1112
  key =>
1087
1113
  requestRemoteCompaction(
1088
1114
  endpoint,
1089
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText },
1115
+ { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, prompt: promptText, maxTokens },
1090
1116
  signal,
1091
1117
  { fetch: options?.fetch, model, apiKey: key },
1092
1118
  ),
@@ -1657,7 +1683,7 @@ async function generateTurnPrefixSummary(
1657
1683
  signal?: AbortSignal,
1658
1684
  options?: SummaryOptions,
1659
1685
  ): Promise<string> {
1660
- const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
1686
+ const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), MAX_SUMMARY_TOKENS); // Smaller budget for turn prefix
1661
1687
 
1662
1688
  const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
1663
1689
  const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));
@@ -239,6 +239,7 @@ export interface OpenAiRemoteCompactionResponse extends OpenAiRemoteCompactionPr
239
239
  export interface RemoteCompactionRequest {
240
240
  systemPrompt: string;
241
241
  prompt: string;
242
+ maxTokens?: number;
242
243
  }
243
244
 
244
245
  export interface RemoteCompactionResponse {
@@ -928,8 +929,9 @@ export async function requestRemoteCompaction(
928
929
  { role: "user", content: request.prompt },
929
930
  ],
930
931
  stream: false,
932
+ max_tokens: request.maxTokens,
931
933
  }
932
- : { systemPrompt: request.systemPrompt, prompt: request.prompt };
934
+ : { systemPrompt: request.systemPrompt, prompt: request.prompt, maxTokens: request.maxTokens };
933
935
 
934
936
  const response = await (opts?.fetch ?? fetch)(endpoint, {
935
937
  method: "POST",