Skip to content

feat(agent-core-v2): add tokenCounting service with strategy config and measured anchors - #2563

Merged
sailist merged 3 commits into
MoonshotAI:mainfrom
sailist:feat/token-counting
Aug 4, 2026
Merged

feat(agent-core-v2): add tokenCounting service with strategy config and measured anchors#2563
sailist merged 3 commits into
MoonshotAI:mainfrom
sailist:feat/token-counting

Conversation

@sailist

@sailist sailist commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

None — the problem is explained in the next section.

Problem

Token counting in the v2 engine had three issues:

  1. No user control over where token counts come from. The character-based estimate was always on, with no way to rely on provider-reported usage only — or to fall back to estimates only for providers that do not report usage (for which the engine anchored emptyUsage() zeros, zeroing the context size and silently disabling compaction).
  2. Counting logic was scattered across contextSize, compaction trigger/budget helpers, and a private request-size estimate in full compaction, each interpreting sizes its own way.
  3. Undo and compaction threw away real measurements. The model kept only the latest measured {length, tokens} pair: undoing past it rebased the surviving (previously measured) prefix to a fresh estimate, and compaction adopted an estimated tokensAfter as the new "measured" value.

What changed

  • Unified IAgentTokenCountingService (agent/tokenCounting) as the single owner of every token count — context size, full-request size (system prompt + tools + messages), and estimate primitives — replacing the split IAgentContextSizeService / IAgentTokenEstimateService / full-compaction-local paths. All consumers (contextMemory, fullCompaction, llmRequester, rpc, mirrorAgentRun, sessionLegacy, kap-server legacyStatus, node-sdk session wiring, kimi-inspect panels) now go through it; the v1 edge bridges no longer read the wire model directly.
  • [token_counting] config section with strategy = "measured+estimated" | "measured" | "estimated" (default measured+estimated) and the KIMI_TOKEN_COUNTING_STRATEGY env override. measured reads all estimates as 0 (decisions rely on real usage, compaction splits degrade to count-based); estimated ignores measured anchors and estimates everything — the escape hatch for providers without usage reporting.
  • Measured-anchor ledger in TokenCountingModel (live-only, persist: false — the wire format is unchanged): every LLM exchange writes a real anchor, undo truncates the ledger so the surviving prefix restores its REAL measured size instead of a re-estimate, and compaction rebases to a single anchor whose summary component is the compaction exchange's measured output tokens (persisted as summaryOutputTokens on the compaction record for replay).
  • No anchor without usage: a stream that never reports a usage event no longer writes a zero measured anchor, so providers without usage reporting keep estimate-based sizing instead of a zeroed context.
  • rpc getContext returns the strategy-resolved size (measured + estimated tail) instead of measured, keeping the v1 context.tokenCount semantics correct under the estimated strategy (where measured alone reads 0).

Verified end-to-end with the klient memory transport against a real provider: under measured+estimated and measured the reported tokenCount exactly equals provider-reported usage after each turn; under estimated it exactly equals the character heuristic and ignores provider usage. Known limits: provider usage is exchange-granular (real values exist at exchange boundaries only), and anchors are live-only (resume estimates until the next exchange, as before).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 588e093

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@5ffa50a
npx https://pkg.pr.new/@moonshot-ai/kimi-code@5ffa50a

commit: 5ffa50a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f1cbd1a7d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// rebases the measured model first, so the max only wins in that window.
const measured = wire.getModel(ContextSizeModel);
const contextTokens = Math.max(contextSize.get().size, measured.tokens);
const contextTokens = Math.max(tokenCounting.get().size, tokenCounting.latestMeasured());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor estimated token counting in status snapshots

When a user sets [token_counting].strategy = "estimated" to ignore unreliable provider usage, tokenCounting.get().size returns the heuristic estimate but this Math.max immediately reintroduces the last provider-reported anchor. If that provider usage is bogus and higher than the estimate, legacy REST/WS status still reports the provider number for contextTokens (the same calculation is mirrored in packages/node-sdk/src/v2/session-wiring.ts:282), so the context-size display contradicts the new strategy. Gate latestMeasured() by strategy here or have the service expose the correct status count.

Useful? React with 👍 / 👎.

historyForModel = shrinkCompactionHistoryAfterOverflow(
messagesToCompact,
overflowShrinkCount,
(message) => this.tokenCounting.estimateMessage(message),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use ungated estimates when shrinking overflowed compactions

When [token_counting].strategy = "measured", IAgentTokenCountingService.estimateMessage() returns 0 by design. Passing that gated estimator into shrinkCompactionHistoryAfterOverflow makes totalTokens and tokenBudget both 0, and takeRecentMessagesWithinTokenBudget keeps every message because zero-sized messages never exceed the zero budget. Any compaction request that gets a 413 is therefore retried with the same messagesToCompact until the attempt limit, so measured-only users cannot recover from overlarge compaction prompts; this shrink path needs the raw heuristic estimator or another count-based fallback.

Useful? React with 👍 / 👎.

sailist added 3 commits August 4, 2026 09:23
…nd measured anchors

- add IAgentTokenCountingService as the single owner of token counts:
  context size, full-request size, and estimate primitives, replacing
  the scattered contextSize/tokenEstimate/fullCompaction paths
- add [token_counting] config section with strategy = measured+estimated
  (default) / measured / estimated, plus the KIMI_TOKEN_COUNTING_STRATEGY
  env override; measured zeroes all estimates, estimated ignores anchors
- keep a live measured-anchor ledger in TokenCountingModel: each LLM
  exchange writes a real anchor, undo truncates the ledger so the
  surviving prefix restores its REAL measured size instead of a
  re-estimate, and compaction rebases to a single anchor that blends the
  compaction exchange's measured summary output tokens
- skip writing an anchor when the stream reports no usage event instead
  of anchoring emptyUsage() zeros, which zeroed the context size and
  silenced compaction for providers without usage reporting
- return the strategy-resolved size (not measured) from rpc getContext
  so the tokenCount contract stays correct under the estimated strategy
- migrate all consumers (contextMemory, fullCompaction, llmRequester,
  rpc, mirrorAgentRun, sessionLegacy, kap-server legacyStatus, node-sdk,
  kimi-inspect) to the new service; edge bridges no longer read the wire
  model directly
- document [token_counting] and KIMI_TOKEN_COUNTING_STRATEGY in the
  bilingual config reference
- readLegacyStatus falls back to the default model's context limit when no
  model is bound, and omits maxContextTokens entirely when the limit is
  unknown (0 is the engine's UNKNOWN_CAPABILITY marker, not a real limit)
- profileService no longer emits maxContextTokens in agent.status.updated
  when the bound model alias does not resolve
…ting edge

- keep measured anchors and heuristic estimates both recorded and feeding
  internal logic (compaction triggers, budgets, overflow backoff) regardless
  of the configured strategy
- add IAgentTokenCountingService.statusSize() as the single strategy-resolved
  outward reading and route the WS/REST/RPC status surfaces through it
- fix the context-size display falling back to provider-reported usage under
  the estimated strategy
- fix compaction overflow backoff retrying identical messages until failure
  under the measured strategy (the strategy-gated estimator read as 0)
@sailist
sailist force-pushed the feat/token-counting branch from 8d61a3d to 588e093 Compare August 4, 2026 01:44
@sailist
sailist merged commit 2118544 into MoonshotAI:main Aug 4, 2026
11 of 12 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant