@sayknow-cli/agent-core 0.2.4 → 0.2.6

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
+ ## [0.6.2] - 2026-06-19
6
+
7
+ ### Changed
8
+
9
+ - Token accounting no longer depends on a native embedded tokenizer. Token usage now anchors on provider-reported usage (`calculatePromptTokens`) and estimates only the unsent delta with a cheap heuristic (~chars/4 × 1.2); emergency compaction floors are unchanged. Compaction, branch summarization, and fork-seed paths were repointed off the removed native token-estimate alias. Part of dropping the bundled tiktoken/o200k tokenizer (#879).
10
+
5
11
  ## [0.5.4] - 2026-06-17
6
12
 
7
13
  ### Fixed
@@ -99,33 +99,16 @@ export declare const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLim
99
99
  export declare function emergencyCompactionReason(sample: EmergencyCompactionSample, limits?: EmergencyCompactionLimits): CompactionTriggerReason | null;
100
100
  export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): number;
101
101
  /**
102
- * Estimate token count for a message using the native o200k tokenizer.
103
- * Exact for o200k only; an approximation for Anthropic/other model families
104
- * (Anthropic doesn't publish a tokenizer) within ~5–10% on English/code text.
105
- *
106
- * This materializes the native BPE table (~50MB RSS) on first call. Use it
107
- * only for context-changing decisions (compaction trigger/cut points, pruning
108
- * budgets, branch summarization, fork-context seeding, context-limit
109
- * enforcement). For display-only totals use
110
- * {@link estimateMessageTokensHeuristic}.
111
- */
112
- export declare function countMessageTokensNativeO200k(message: AgentMessage): number;
113
- /**
114
- * Backwards-compatible alias for {@link countMessageTokensNativeO200k}.
115
- * Existing callers treat this as the canonical message-token estimator for
116
- * context-changing decisions.
117
- */
118
- export declare const estimateTokens: typeof countMessageTokensNativeO200k;
119
- /**
120
- * Cheap, native-free token estimate for a message. Suitable ONLY for
121
- * display/init surfaces (status line, /context report, HUD totals) — never
122
- * for context-changing decisions, which must use
123
- * {@link countMessageTokensNativeO200k}.
102
+ * Native-free chars/4 token estimate for a message. This is the only message
103
+ * token estimator: provider usage (see {@link calculatePromptTokens}) anchors
104
+ * the already-sent context, and this covers unsent/trailing deltas, per-entry
105
+ * budgeting, and display surfaces. Callers add a conservative inflation factor
106
+ * where compaction-threshold safety requires it.
124
107
  */
125
108
  export declare function estimateMessageTokensHeuristic(message: AgentMessage): number;
126
109
  /**
127
- * Cheap, native-free token estimate for plain string fragments. Display-only
128
- * counterpart of the native `countTokens(fragments)` aggregate.
110
+ * Native-free chars/4 token estimate for plain string fragments. Fragment-level
111
+ * counterpart of {@link estimateMessageTokensHeuristic}.
129
112
  */
130
113
  export declare function estimateTextTokensHeuristic(fragments: string | readonly string[]): number;
131
114
  export declare function estimateEntryTokens(entry: SessionEntry): number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/agent-core",
4
- "version": "0.2.4",
4
+ "version": "0.2.6",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://github.com/jaybeyond/Sayknow_CLI",
7
7
  "author": "jaybeyond",
@@ -35,9 +35,9 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@sayknow-cli/ai": "0.2.4",
39
- "@sayknow-cli/natives": "0.2.4",
40
- "@sayknow-cli/utils": "0.2.4",
38
+ "@sayknow-cli/ai": "0.2.6",
39
+ "@sayknow-cli/natives": "0.2.6",
40
+ "@sayknow-cli/utils": "0.2.6",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
@@ -9,7 +9,7 @@ import type { Model, ProviderSessionState } from "@sayknow-cli/ai";
9
9
  import { prompt } from "@sayknow-cli/utils";
10
10
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
11
11
  import type { AgentMessage } from "../types";
12
- import { estimateTokens } from "./compaction";
12
+ import { estimateMessageTokensHeuristic } from "./compaction";
13
13
  import type { ReadonlySessionManager, SessionEntry } from "./entries";
14
14
  import {
15
15
  type ConvertToLlm,
@@ -243,7 +243,7 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
243
243
  // Extract file ops from assistant messages (tool calls)
244
244
  extractFileOpsFromMessage(message, fileOps);
245
245
 
246
- const tokens = estimateTokens(message);
246
+ const tokens = estimateMessageTokensHeuristic(message);
247
247
 
248
248
  // Check budget before adding
249
249
  if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
@@ -5,7 +5,6 @@
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
7
 
8
- import { createRequire } from "node:module";
9
8
  import {
10
9
  type AssistantMessage,
11
10
  Effort,
@@ -15,7 +14,7 @@ import {
15
14
  type ProviderSessionState,
16
15
  type Usage,
17
16
  } from "@sayknow-cli/ai";
18
- import { isCompiledBinary, logger, prompt } from "@sayknow-cli/utils";
17
+ import { logger, prompt } from "@sayknow-cli/utils";
19
18
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
20
19
  import type { AgentMessage, AgentTool } from "../types";
21
20
  import type { CompactionEntry, SessionEntry } from "./entries";
@@ -317,86 +316,17 @@ export function resolveThresholdTokens(
317
316
  * matching what providers typically bill for inline images.
318
317
  */
319
318
  const IMAGE_TOKEN_ESTIMATE = 1200;
320
- const SOURCE_NATIVE_TOKENIZER_ENTRYPOINT = "../../../natives/native/index.js";
321
- const COMPILED_NATIVE_TOKENIZER_ENTRYPOINT = "/$bunfs/root/packages/natives/native/index.js";
322
-
323
- const requireFromCompaction = createRequire(import.meta.url);
324
-
325
- interface NativeTokenizerModule {
326
- countTokens(input: string | string[], encoding?: unknown): number;
327
- }
328
-
329
319
  /**
330
- * Lazily-required native `countTokens`. `@sayknow-cli/natives` dlopens a ~39MB
331
- * addon; importing it at module scope would put that cost on every cold path
332
- * that touches compaction exports (status line, print mode, context report).
333
- * Deferring the require to the first context-changing call keeps display paths
334
- * native-free.
335
- *
336
- * Do not resolve this via a package-name dynamic require of
337
- * `@sayknow-cli/natives`: Bun standalone binaries cannot satisfy those from
338
- * `$bunfs`. The sibling-package source path is stable for workspace and
339
- * package-install layouts:
340
- *
341
- * - workspace: `packages/agent` -> `packages/natives`
342
- * - npm/bun install: `node_modules/@sayknow-cli/agent-core` ->
343
- * `node_modules/@sayknow-cli/natives`
344
- *
345
- * Bun rewrites `createRequire(import.meta.url)` to the compiled executable
346
- * root (`/$bunfs/root/skc-*`) in standalone binaries, so compiled mode uses the
347
- * absolute bunfs module path emitted by the binary build scripts.
320
+ * Estimate tokens for collected message fragments using the native-free
321
+ * heuristic. Provider usage is the authoritative anchor for context-changing
322
+ * decisions (see {@link calculatePromptTokens}); this chars/4 estimate covers
323
+ * only unsent/trailing deltas and per-entry budgeting, and callers add a
324
+ * conservative inflation factor where threshold safety requires it.
348
325
  */
349
- let cachedNativeCountTokens: ((input: string | string[], encoding?: unknown) => number) | null = null;
350
-
351
- function nativeTokenizerEntrypoint(): string {
352
- return isCompiledBinary() ? COMPILED_NATIVE_TOKENIZER_ENTRYPOINT : SOURCE_NATIVE_TOKENIZER_ENTRYPOINT;
353
- }
354
-
355
- /** Max total fragment chars sent to the synchronous native tokenizer (F22). */
356
- const MAX_NATIVE_TOKENIZE_CHARS = 2 * 1024 * 1024;
357
-
358
- function nativeCountTokens(fragments: string[]): number {
359
- let totalChars = 0;
360
- for (const fragment of fragments) totalChars += fragment.length;
361
- if (totalChars > MAX_NATIVE_TOKENIZE_CHARS) {
362
- // F22: skip the synchronous native BPE tokenizer (materializes a ~39MB table and is
363
- // O(text)) on pathologically large inputs; the cheap chars/token heuristic is more
364
- // than accurate enough for size/budget decisions and never blocks the event loop.
365
- return estimateTextTokensHeuristic(fragments);
366
- }
367
- if (!cachedNativeCountTokens) {
368
- const natives = requireFromCompaction(nativeTokenizerEntrypoint()) as NativeTokenizerModule;
369
- cachedNativeCountTokens = natives.countTokens;
370
- }
371
- return cachedNativeCountTokens(fragments);
372
- }
373
-
374
326
  function countCollectedMessageFragments(collected: { fragments: string[]; extra: number }): number {
375
- return nativeCountTokens(collected.fragments) + collected.extra;
327
+ return estimateTextTokensHeuristic(collected.fragments) + collected.extra;
376
328
  }
377
329
 
378
- /**
379
- * Estimate token count for a message using the native o200k tokenizer.
380
- * Exact for o200k only; an approximation for Anthropic/other model families
381
- * (Anthropic doesn't publish a tokenizer) within ~5–10% on English/code text.
382
- *
383
- * This materializes the native BPE table (~50MB RSS) on first call. Use it
384
- * only for context-changing decisions (compaction trigger/cut points, pruning
385
- * budgets, branch summarization, fork-context seeding, context-limit
386
- * enforcement). For display-only totals use
387
- * {@link estimateMessageTokensHeuristic}.
388
- */
389
- export function countMessageTokensNativeO200k(message: AgentMessage): number {
390
- return countCollectedMessageFragments(collectMessageFragments(message));
391
- }
392
-
393
- /**
394
- * Backwards-compatible alias for {@link countMessageTokensNativeO200k}.
395
- * Existing callers treat this as the canonical message-token estimator for
396
- * context-changing decisions.
397
- */
398
- export const estimateTokens = countMessageTokensNativeO200k;
399
-
400
330
  /**
401
331
  * Average bytes per token for the cheap heuristic. ~4 bytes/token is the
402
332
  * conventional approximation for English/code text under modern BPE
@@ -406,10 +336,11 @@ export const estimateTokens = countMessageTokensNativeO200k;
406
336
  const HEURISTIC_BYTES_PER_TOKEN = 4;
407
337
 
408
338
  /**
409
- * Cheap, native-free token estimate for a message. Suitable ONLY for
410
- * display/init surfaces (status line, /context report, HUD totals) — never
411
- * for context-changing decisions, which must use
412
- * {@link countMessageTokensNativeO200k}.
339
+ * Native-free chars/4 token estimate for a message. This is the only message
340
+ * token estimator: provider usage (see {@link calculatePromptTokens}) anchors
341
+ * the already-sent context, and this covers unsent/trailing deltas, per-entry
342
+ * budgeting, and display surfaces. Callers add a conservative inflation factor
343
+ * where compaction-threshold safety requires it.
413
344
  */
414
345
  export function estimateMessageTokensHeuristic(message: AgentMessage): number {
415
346
  const { fragments, extra } = collectMessageFragments(message);
@@ -421,8 +352,8 @@ export function estimateMessageTokensHeuristic(message: AgentMessage): number {
421
352
  }
422
353
 
423
354
  /**
424
- * Cheap, native-free token estimate for plain string fragments. Display-only
425
- * counterpart of the native `countTokens(fragments)` aggregate.
355
+ * Native-free chars/4 token estimate for plain string fragments. Fragment-level
356
+ * counterpart of {@link estimateMessageTokensHeuristic}.
426
357
  */
427
358
  export function estimateTextTokensHeuristic(fragments: string | readonly string[]): number {
428
359
  if (typeof fragments === "string") return Math.ceil(fragments.length / HEURISTIC_BYTES_PER_TOKEN);