@oh-my-pi/pi-agent-core 18.0.10 → 18.0.11

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.0.11] - 2026-08-29
6
+
7
+ ### Fixed
8
+
9
+ - Fixed agent startup and context compaction failures for models with unrecognized tokenizer encodings.
10
+
5
11
  ## [18.0.10] - 2026-08-28
6
12
 
7
13
  ### Added
@@ -1,11 +1,12 @@
1
1
  import type { Model } from "@oh-my-pi/pi-ai";
2
- import { Encoding } from "@oh-my-pi/pi-natives";
2
+ import * as natives from "@oh-my-pi/pi-natives";
3
3
  import type { AgentMessage } from "./types.js";
4
4
  /** Maps the catalog-resolved tokenizer family to its native implementation. */
5
- export declare function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): Encoding | null;
5
+ export declare function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): natives.Encoding | null;
6
6
  /**
7
- * `strict` always pays for an exact native count (the catalog-resolved
8
- * tokenizer when known, o200k_base otherwise). `approximate` and
7
+ * `strict` always tries an exact native count (the catalog-resolved tokenizer
8
+ * when known, o200k_base otherwise), falling back to the byte upper bound when
9
+ * the loaded addon does not recognize that encoding. `approximate` and
9
10
  * `upperbound` prefer the same exact count for known tokenizer families or
10
11
  * when `PI_TOKENIZER_ACCURATE=1` is set; otherwise they use a cheap heuristic:
11
12
  * `approximate` a bytes/4 guess, `upperbound` the raw byte length (never
@@ -31,11 +32,10 @@ export interface TokenBudgetCheck {
31
32
  fits: boolean;
32
33
  /**
33
34
  * Token count behind the verdict: the exact native count when `exact` is
34
- * set, otherwise the cheap byte upper bound (which already fit, so it is
35
- * only an over-estimate of a count known to be under budget).
35
+ * set, otherwise the conservative byte upper bound.
36
36
  */
37
37
  tokens: number;
38
- /** Whether the exact tokenizer had to run because the cheap bound busted. */
38
+ /** Whether `tokens` came from the exact native tokenizer. */
39
39
  exact: boolean;
40
40
  }
41
41
  /**
@@ -50,7 +50,7 @@ export interface TokenBudgetCheck {
50
50
  export declare class Tokenizer {
51
51
  #private;
52
52
  constructor(model?: Pick<Model, "tokenizer"> | null);
53
- get encoding(): Encoding | null;
53
+ get encoding(): natives.Encoding | null;
54
54
  countTokens(text: string | string[], mode?: TokenCountMode): number;
55
55
  /**
56
56
  * Cheap-first budget probe — the way to ask "does this fit in `budget`
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.0.10",
4
+ "version": "18.0.11",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,16 +35,16 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.0.10",
39
- "@oh-my-pi/pi-catalog": "18.0.10",
40
- "@oh-my-pi/pi-natives": "18.0.10",
41
- "@oh-my-pi/pi-utils": "18.0.10",
42
- "@oh-my-pi/pi-wire": "18.0.10",
43
- "@oh-my-pi/snapcompact": "18.0.10",
38
+ "@oh-my-pi/pi-ai": "18.0.11",
39
+ "@oh-my-pi/pi-catalog": "18.0.11",
40
+ "@oh-my-pi/pi-natives": "18.0.11",
41
+ "@oh-my-pi/pi-utils": "18.0.11",
42
+ "@oh-my-pi/pi-wire": "18.0.11",
43
+ "@oh-my-pi/snapcompact": "18.0.11",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.0.10",
47
+ "@oh-my-pi/omptype": "18.0.11",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"
package/src/tokenizer.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Model } from "@oh-my-pi/pi-ai";
2
2
  import type { ModelTokenizer } from "@oh-my-pi/pi-catalog/types";
3
- import { countTokens as countTokensNat, Encoding } from "@oh-my-pi/pi-natives";
3
+ import * as natives from "@oh-my-pi/pi-natives";
4
4
  import { stringifyJson } from "@oh-my-pi/pi-utils";
5
5
  import * as snapcompact from "@oh-my-pi/snapcompact";
6
6
  import { isEstimateCacheable, messageEstimateVersion } from "./compaction/message-cache";
@@ -9,25 +9,26 @@ import type { AgentMessage } from "./types";
9
9
  const testEnv = Bun.env.NODE_ENV === "test";
10
10
  const accurate = process.env.PI_TOKENIZER_ACCURATE === "1" && !testEnv;
11
11
 
12
- const NATIVE_ENCODING: Record<ModelTokenizer, Encoding> = {
13
- "claude-v3": Encoding.ClaudeV3,
14
- "claude-v47": Encoding.ClaudeV47,
15
- "claude-v5": Encoding.ClaudeV5,
16
- "claude-v5-sonnet": Encoding.ClaudeV5Sonnet,
17
- qwen3: Encoding.Qwen3,
18
- "deepseek-v3": Encoding.DeepSeekV3,
19
- "kimi-k2": Encoding.KimiK2,
20
- glm5: Encoding.Glm5,
12
+ const NATIVE_ENCODING: Record<ModelTokenizer, natives.Encoding> = {
13
+ "claude-v3": natives.Encoding.ClaudeV3,
14
+ "claude-v47": natives.Encoding.ClaudeV47,
15
+ "claude-v5": natives.Encoding.ClaudeV5,
16
+ "claude-v5-sonnet": natives.Encoding.ClaudeV5Sonnet,
17
+ qwen3: natives.Encoding.Qwen3,
18
+ "deepseek-v3": natives.Encoding.DeepSeekV3,
19
+ "kimi-k2": natives.Encoding.KimiK2,
20
+ glm5: natives.Encoding.Glm5,
21
21
  };
22
22
 
23
23
  /** Maps the catalog-resolved tokenizer family to its native implementation. */
24
- export function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): Encoding | null {
24
+ export function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): natives.Encoding | null {
25
25
  return model?.tokenizer ? NATIVE_ENCODING[model.tokenizer] : null;
26
26
  }
27
27
 
28
28
  /**
29
- * `strict` always pays for an exact native count (the catalog-resolved
30
- * tokenizer when known, o200k_base otherwise). `approximate` and
29
+ * `strict` always tries an exact native count (the catalog-resolved tokenizer
30
+ * when known, o200k_base otherwise), falling back to the byte upper bound when
31
+ * the loaded addon does not recognize that encoding. `approximate` and
31
32
  * `upperbound` prefer the same exact count for known tokenizer families or
32
33
  * when `PI_TOKENIZER_ACCURATE=1` is set; otherwise they use a cheap heuristic:
33
34
  * `approximate` a bytes/4 guess, `upperbound` the raw byte length (never
@@ -61,17 +62,41 @@ function sumFragments(text: string | string[], perFragment: (t: string) => numbe
61
62
  return Array.isArray(text) ? text.reduce((sum, t) => sum + perFragment(t), 0) : perFragment(text);
62
63
  }
63
64
 
65
+ interface NativeTokenCount {
66
+ tokens: number;
67
+ exact: boolean;
68
+ }
69
+
70
+ function countTokensNat(
71
+ text: string | string[],
72
+ encoding: natives.Encoding | null | undefined,
73
+ mode: TokenCountMode,
74
+ ): NativeTokenCount {
75
+ try {
76
+ return { tokens: natives.countTokens(text, encoding), exact: true };
77
+ } catch (error) {
78
+ if (
79
+ !(error instanceof Error) ||
80
+ (!error.message.includes("does not match any variant of enum") &&
81
+ !error.message.includes("unknown enum variant"))
82
+ ) {
83
+ throw error;
84
+ }
85
+ const tokens = sumFragments(text, mode === "approximate" ? byteEstimate : byteLength);
86
+ return { tokens, exact: false };
87
+ }
88
+ }
89
+
64
90
  /** Verdict from {@link Tokenizer.checkTokenBudget}. */
65
91
  export interface TokenBudgetCheck {
66
92
  /** Whether the text fits the budget. */
67
93
  fits: boolean;
68
94
  /**
69
95
  * Token count behind the verdict: the exact native count when `exact` is
70
- * set, otherwise the cheap byte upper bound (which already fit, so it is
71
- * only an over-estimate of a count known to be under budget).
96
+ * set, otherwise the conservative byte upper bound.
72
97
  */
73
98
  tokens: number;
74
- /** Whether the exact tokenizer had to run because the cheap bound busted. */
99
+ /** Whether `tokens` came from the exact native tokenizer. */
75
100
  exact: boolean;
76
101
  }
77
102
 
@@ -104,7 +129,7 @@ interface MessageEstimate {
104
129
  * `PI_TOKENIZER_ACCURATE=1`).
105
130
  */
106
131
  export class Tokenizer {
107
- readonly #encoding: Encoding | null;
132
+ readonly #encoding: natives.Encoding | null;
108
133
 
109
134
  /**
110
135
  * Per-message estimate memo. Keyed by message identity, deliberately not a
@@ -120,14 +145,14 @@ export class Tokenizer {
120
145
  this.#encoding = tokenizerEncodingForModel(model);
121
146
  }
122
147
 
123
- get encoding(): Encoding | null {
148
+ get encoding(): natives.Encoding | null {
124
149
  return this.#encoding;
125
150
  }
126
151
 
127
152
  countTokens(text: string | string[], mode: TokenCountMode = "approximate"): number {
128
- if (mode === "strict") return countTokensNat(text, this.#encoding);
129
- if (!testEnv && this.#encoding !== null) return countTokensNat(text, this.#encoding);
130
- if (accurate) return countTokensNat(text);
153
+ if (mode === "strict") return countTokensNat(text, this.#encoding, mode).tokens;
154
+ if (!testEnv && this.#encoding !== null) return countTokensNat(text, this.#encoding, mode).tokens;
155
+ if (accurate) return countTokensNat(text, undefined, mode).tokens;
131
156
  return sumFragments(text, mode === "upperbound" ? byteLength : byteEstimate);
132
157
  }
133
158
 
@@ -145,8 +170,8 @@ export class Tokenizer {
145
170
  checkTokenBudget(text: string | string[], budget: number): TokenBudgetCheck {
146
171
  const bound = sumFragments(text, byteLength);
147
172
  if (bound <= budget) return { fits: true, tokens: bound, exact: false };
148
- const tokens = this.countTokens(text, "strict");
149
- return { fits: tokens <= budget, tokens, exact: true };
173
+ const result = countTokensNat(text, this.#encoding, "strict");
174
+ return { fits: result.tokens <= budget, tokens: result.tokens, exact: result.exact };
150
175
  }
151
176
 
152
177
  /**