@oh-my-pi/pi-agent-core 18.8.6 → 18.8.8

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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.8.8] - 2026-10-10
6
+
7
+ ### Added
8
+
9
+ - Added `Agent.setModelResolver()` to fit every model an agent adopts (via `setModel`, starting with the current one) before it is used ([#15048](https://github.com/can1357/oh-my-pi/pull/15048) by [@H4vC](https://github.com/H4vC))
10
+ - Added `CompactionSettings.baseWindowTokens`: when set, `resolveThresholdTokens()` scales its percentage or reserve-based threshold from that base instead of the full context window ([#15065](https://github.com/can1357/oh-my-pi/pull/15065) by [@H4vC](https://github.com/H4vC))
11
+
12
+ ### Fixed
13
+
14
+ - Fixed OpenAI V1 remote compaction re-attaching a prior Anthropic native payload after a successful compact ([#15041](https://github.com/can1357/oh-my-pi/pull/15041) by [@PaleRoses](https://github.com/PaleRoses))
15
+ - Fixed `resolveThresholdTokens()` clamping a positive `thresholdTokens` to `baseWindowTokens`; a fixed threshold is checked against the real window, and the base only rescales the percentage and reserve policies ([#15074](https://github.com/can1357/oh-my-pi/pull/15074) by [@H4vC](https://github.com/H4vC))
16
+ - Fixed `resolveThresholdTokens()` clamping a fixed `thresholdTokens` at or past the context window to one token below it, so compaction fired only after the next request overflowed; it now clamps to the window less the reserve ([#15146](https://github.com/can1357/oh-my-pi/pull/15146) by [@will-bogusz](https://github.com/will-bogusz))
17
+
5
18
  ## [18.8.6] - 2026-10-08
6
19
 
7
20
  ### Added
@@ -469,6 +469,12 @@ export declare class Agent {
469
469
  emitExternalEvent(event: AgentEvent): void;
470
470
  setSystemPrompt(v: string[] | string): void;
471
471
  setModel(model: Model): void;
472
+ /**
473
+ * Route every model this agent adopts through `resolver` (e.g. to fit a
474
+ * shared catalog row to this agent's own settings), starting with the
475
+ * current one; `undefined` adopts models as given from now on.
476
+ */
477
+ setModelResolver(resolver: ((model: Model) => Model) | undefined): void;
472
478
  setThinkingLevel(l: Effort | undefined): void;
473
479
  setDisableReasoning(disabled: boolean): void;
474
480
  setSteeringMode(mode: "all" | "one-at-a-time"): void;
@@ -34,6 +34,14 @@ export interface CompactionSettings {
34
34
  strategy?: "context-full" | "handoff" | "shake" | "snapcompact" | "off";
35
35
  thresholdPercent?: number;
36
36
  thresholdTokens?: number;
37
+ /**
38
+ * Stands in for the context window when the percentage or reserve-based
39
+ * threshold is computed (when `> 0` and smaller than the window); a positive
40
+ * `thresholdTokens` still wins and is checked against the real window (at
41
+ * or past it, the window less its reserve applies). Set by per-model
42
+ * compaction limits; request and overflow budgets keep the real window.
43
+ */
44
+ baseWindowTokens?: number;
37
45
  midTurnEnabled?: boolean;
38
46
  /**
39
47
  * Tokens reserved below the context window for the next prompt + response.
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": "18.8.6",
4
+ "version": "18.8.8",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -38,16 +38,16 @@
38
38
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
39
39
  },
40
40
  "dependencies": {
41
- "@oh-my-pi/pi-ai": "18.8.6",
42
- "@oh-my-pi/pi-catalog": "18.8.6",
43
- "@oh-my-pi/pi-natives": "18.8.6",
44
- "@oh-my-pi/pi-utils": "18.8.6",
45
- "@oh-my-pi/pi-wire": "18.8.6",
46
- "@oh-my-pi/snapcompact": "18.8.6",
41
+ "@oh-my-pi/pi-ai": "18.8.8",
42
+ "@oh-my-pi/pi-catalog": "18.8.8",
43
+ "@oh-my-pi/pi-natives": "18.8.8",
44
+ "@oh-my-pi/pi-utils": "18.8.8",
45
+ "@oh-my-pi/pi-wire": "18.8.8",
46
+ "@oh-my-pi/snapcompact": "18.8.8",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.8.6",
50
+ "@oh-my-pi/omptype": "18.8.8",
51
51
  "@opentelemetry/context-async-hooks": "^2.9.0",
52
52
  "@opentelemetry/sdk-trace-base": "^2.9.0",
53
53
  "@types/bun": "^1.3.14"
package/src/agent.ts CHANGED
@@ -453,6 +453,7 @@ export class Agent {
453
453
  #promptCacheKey?: string;
454
454
  #metadata?: Record<string, unknown>;
455
455
  #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
456
+ #modelResolver?: (model: Model) => Model;
456
457
  #providerSessionState?: Map<string, ProviderSessionState>;
457
458
  #thinkingBudgets?: ThinkingBudgets;
458
459
  #temperature?: number;
@@ -1213,8 +1214,20 @@ export class Agent {
1213
1214
  }
1214
1215
 
1215
1216
  setModel(model: Model) {
1216
- this.#state.model = model;
1217
- this.#syncTokenizer(model);
1217
+ // Sessions may start with no model selected (`initialState.model: undefined`).
1218
+ const resolved = this.#modelResolver && model ? this.#modelResolver(model) : model;
1219
+ this.#state.model = resolved;
1220
+ this.#syncTokenizer(resolved);
1221
+ }
1222
+
1223
+ /**
1224
+ * Route every model this agent adopts through `resolver` (e.g. to fit a
1225
+ * shared catalog row to this agent's own settings), starting with the
1226
+ * current one; `undefined` adopts models as given from now on.
1227
+ */
1228
+ setModelResolver(resolver: ((model: Model) => Model) | undefined): void {
1229
+ this.#modelResolver = resolver;
1230
+ if (resolver && this.#state.model) this.setModel(this.#state.model);
1218
1231
  }
1219
1232
 
1220
1233
  setThinkingLevel(l: Effort | undefined) {
@@ -163,6 +163,14 @@ export interface CompactionSettings {
163
163
  strategy?: "context-full" | "handoff" | "shake" | "snapcompact" | "off";
164
164
  thresholdPercent?: number;
165
165
  thresholdTokens?: number;
166
+ /**
167
+ * Stands in for the context window when the percentage or reserve-based
168
+ * threshold is computed (when `> 0` and smaller than the window); a positive
169
+ * `thresholdTokens` still wins and is checked against the real window (at
170
+ * or past it, the window less its reserve applies). Set by per-model
171
+ * compaction limits; request and overflow budgets keep the real window.
172
+ */
173
+ baseWindowTokens?: number;
166
174
  midTurnEnabled?: boolean;
167
175
  /**
168
176
  * Tokens reserved below the context window for the next prompt + response.
@@ -358,27 +366,42 @@ export function compactionContextTokens(providerContextTokens: number, storedCon
358
366
  return Math.max(Math.max(0, providerContextTokens), Math.max(0, storedConversationEstimate));
359
367
  }
360
368
 
369
+ /**
370
+ * The prompt budget below `contextWindow`: the window less
371
+ * {@link resolveBudgetReserveTokens}, capped at `contextWindow - 1` so it never
372
+ * reaches the whole window even when the reserve resolves to 0.
373
+ */
374
+ function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number {
375
+ return Math.max(0, Math.min(contextWindow - 1, contextWindow - resolveBudgetReserveTokens(contextWindow, settings)));
376
+ }
377
+
361
378
  export function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
362
- // Fixed token limit takes priority over percentage
379
+ // Fixed token limit takes priority over percentage, and is checked against
380
+ // the real window: `baseWindowTokens` only rescales the policies below.
363
381
  const thresholdTokens = settings.thresholdTokens;
364
382
  if (typeof thresholdTokens === "number" && Number.isFinite(thresholdTokens) && thresholdTokens > 0) {
365
- // Clamp to [1, contextWindow - 1] so there's always room
383
+ // A trigger at or past the window can never fire before the request
384
+ // overflows (a provider may cap the window below the configured point);
385
+ // fall back to the window's prompt budget, as the reserve policy does.
386
+ // A trigger below the window is exact, as the `f` model entries promise,
387
+ // even one inside the reserve: the user chose that headroom. The step at
388
+ // the window is inherent; a monotone fallback would sit at window - 1.
389
+ if (thresholdTokens >= contextWindow) return promptBudgetTokens(contextWindow, settings);
366
390
  return Math.min(contextWindow - 1, Math.max(1, thresholdTokens));
367
391
  }
392
+ const baseWindowTokens = settings.baseWindowTokens;
393
+ if (typeof baseWindowTokens === "number" && Number.isFinite(baseWindowTokens) && baseWindowTokens > 0) {
394
+ contextWindow = Math.min(contextWindow, baseWindowTokens);
395
+ }
368
396
 
369
397
  // Percentage-based threshold. The default absolute reserve can exceed bundled
370
398
  // small-context windows, or nearly consume a 16k-class window; in those
371
399
  // known-impossible default configurations, fall back to the proportional
372
400
  // reserve so threshold/recovery-band checks stay usable. Explicit valid
373
- // configured reserves still define the usable prompt budget. Cap at
374
- // contextWindow - 1 (matching the fixed-token clamp above) so the threshold
375
- // never reaches the whole window even when the reserve resolves to 0.
401
+ // configured reserves still define the usable prompt budget.
376
402
  const thresholdPercent = settings.thresholdPercent;
377
403
  if (typeof thresholdPercent !== "number" || !Number.isFinite(thresholdPercent) || thresholdPercent <= 0) {
378
- return Math.max(
379
- 0,
380
- Math.min(contextWindow - 1, contextWindow - resolveBudgetReserveTokens(contextWindow, settings)),
381
- );
404
+ return promptBudgetTokens(contextWindow, settings);
382
405
  }
383
406
  const clampedThresholdPercent = Math.min(99, Math.max(1, thresholdPercent));
384
407
  return Math.floor(contextWindow * (clampedThresholdPercent / 100));
@@ -1845,7 +1868,7 @@ export async function compact(
1845
1868
  ),
1846
1869
  { signal },
1847
1870
  );
1848
- preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, remote);
1871
+ preserveData = withOpenAiRemoteCompactionPreserveData(preserveData, remote);
1849
1872
  usedRemoteCompaction = true;
1850
1873
  } catch (err) {
1851
1874
  // A user/session abort is a cancellation, not a remote failure —