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

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.1.0",
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.",
@@ -27,24 +27,24 @@
27
27
  "main": "./src/index.ts",
28
28
  "types": "./dist/types/index.d.ts",
29
29
  "scripts": {
30
- "check": "biome check . && bun run check:types",
30
+ "check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
31
31
  "check:types": "tsgo -p tsconfig.json --noEmit",
32
- "lint": "biome lint .",
32
+ "lint": "oxlint .",
33
33
  "test": "bun test --parallel",
34
- "fix": "biome check --write --unsafe .",
35
- "fmt": "biome format --write ."
34
+ "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
35
+ "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
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.1.0",
39
+ "@oh-my-pi/pi-catalog": "18.1.0",
40
+ "@oh-my-pi/pi-natives": "18.1.0",
41
+ "@oh-my-pi/pi-utils": "18.1.0",
42
+ "@oh-my-pi/pi-wire": "18.1.0",
43
+ "@oh-my-pi/snapcompact": "18.1.0",
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.1.0",
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/agent-loop.ts CHANGED
@@ -768,7 +768,7 @@ function createDetailedCapture(config: AgentLoopConfig): {
768
768
  const wired: AgentLoopConfig = {
769
769
  ...config,
770
770
  telemetry: {
771
- ...(config.telemetry ?? {}),
771
+ ...config.telemetry,
772
772
  onRunEnd: (summary, coverage) => {
773
773
  captured = { summary, coverage };
774
774
  userHook?.(summary, coverage);
@@ -324,9 +324,7 @@ async function attemptCompactionV2Streaming(
324
324
  ...(request.reasoning || model.useResponsesLite
325
325
  ? {
326
326
  // Lite implies gpt-5.4+, where codex-rs sends `all_turns` replay.
327
- reasoning: model.useResponsesLite
328
- ? { ...(request.reasoning ?? {}), context: "all_turns" }
329
- : request.reasoning,
327
+ reasoning: model.useResponsesLite ? { ...request.reasoning, context: "all_turns" } : request.reasoning,
330
328
  include: ["reasoning.encrypted_content"],
331
329
  }
332
330
  : {}),
@@ -403,7 +401,7 @@ function buildCompactionV2Headers(
403
401
  ? {
404
402
  "content-type": "application/json",
405
403
  "api-key": apiKey,
406
- ...(model.headers ?? {}),
404
+ ...model.headers,
407
405
  }
408
406
  : {
409
407
  "content-type": "application/json",
@@ -1644,7 +1644,7 @@ export async function compact(
1644
1644
  }),
1645
1645
  { signal },
1646
1646
  );
1647
- preserveData = { ...(preserveData ?? {}), ...storeCompactionV2PreserveData(remote, model) };
1647
+ preserveData = { ...preserveData, ...storeCompactionV2PreserveData(remote, model) };
1648
1648
  usedRemoteCompaction = true;
1649
1649
  } catch (err) {
1650
1650
  // A user/session abort is a cancellation, not a remote failure —
@@ -390,7 +390,7 @@ export function withOpenAiRemoteCompactionPreserveData(
390
390
  ): Record<string, unknown> | undefined {
391
391
  if (remoteCompaction) {
392
392
  return {
393
- ...(preserveData ?? {}),
393
+ ...preserveData,
394
394
  [OPENAI_REMOTE_COMPACTION_PRESERVE_KEY]: remoteCompaction,
395
395
  };
396
396
  }
@@ -803,12 +803,12 @@ export async function requestOpenAiRemoteCompaction(
803
803
  ? {
804
804
  "content-type": "application/json",
805
805
  "api-key": apiKey,
806
- ...(model.headers ?? {}),
806
+ ...model.headers,
807
807
  }
808
808
  : {
809
809
  "content-type": "application/json",
810
810
  Authorization: `Bearer ${apiKey}`,
811
- ...(model.headers ?? {}),
811
+ ...model.headers,
812
812
  };
813
813
 
814
814
  // Codex endpoints require additional auth headers
@@ -141,6 +141,7 @@ function estimatePrunedSavings(tokens: number, notice: string): number {
141
141
  * mutations inside the cheap-to-recache tail.
142
142
  */
143
143
  function computeMessageSuffixTokens(entries: readonly SessionEntry[], tokenizer: Tokenizer): number[] {
144
+ // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
144
145
  const suffix = new Array<number>(entries.length);
145
146
  let accumulated = 0;
146
147
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -318,6 +318,7 @@ export function collectShakeRegions(entries: SessionEntry[], tokenizer: Tokenize
318
318
  if (n === 0) return [];
319
319
 
320
320
  // Tokens of all entries strictly more recent than index i.
321
+ // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
321
322
  const accumulatedAfter = new Array<number>(n);
322
323
  let acc = 0;
323
324
  for (let i = n - 1; i >= 0; i--) {
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
  /**
package/src/types.ts CHANGED
@@ -759,8 +759,11 @@ export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = an
759
759
  ) => Promise<AgentToolResult<TDetails, TParameters>>;
760
760
 
761
761
  // AgentTool extends Tool but adds the execute function
762
- export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown>
763
- extends Tool<TParameters> {
762
+ export interface AgentTool<
763
+ TParameters extends TSchema = TSchema,
764
+ TDetails = any,
765
+ TTheme = unknown,
766
+ > extends Tool<TParameters> {
764
767
  // A human-readable label for the tool to be displayed in UI
765
768
  label: string;
766
769
  /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */