kimi-code / packages /agent-core-v2 /src /session /tokenCounting /tokenCountingAgentModel.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw
History Blame Contribute Delete
7.5 kB
import { z } from 'zod';
import { contextMemoryKey } from '#/agent/contextMemory/contextOps';
import type { ContextMessage } from '#/agent/contextMemory/types';
import type { ContextSize, TokenCountingStrategy } from '#/agent/tokenCounting/tokenCounting';
import {
anchorsEqual,
normalizeAnchorLength,
TokenCountingMeasured,
TokenCountingRebased,
TokenCountingTruncated,
TokenCountingTurnRecorded,
type TokenAnchor,
type TokenCountingState,
} from '#/agent/tokenCounting/tokenCountingOps';
import { AgentStatusUpdated } from '#/agent/usage/usageEvents';
import type { Message } from '#/llm-adapter/contract/message';
import { estimateTokensForMessages } from '#/llm-adapter/contract/tokens';
import type { TokenUsage } from '#human/llm/usage';
import { AgentModel, defineAgentModel, type AgentModelContext } from '#/state/agentModel';
import type { TokenCountingRebaseInput } from './sessionTokenCounting';
const ZERO_ANCHOR: TokenAnchor = { length: 0, tokens: 0, measured: true };
export class TokenCountingAgentModel extends AgentModel<TokenCountingState> {
constructor(context: AgentModelContext) {
super(context);
this.on(TokenCountingMeasured, (event) => {
const length = normalizeAnchorLength(event.length);
const tokens = Math.max(0, event.tokens);
const anchor: TokenAnchor = { length, tokens, measured: true };
const anchors = [...this.state.anchors.filter((a) => a.length < length), anchor];
if (!(this.state.tokens === tokens && anchorsEqual(this.state.anchors, anchors))) {
this.state.anchors = anchors;
this.state.tokens = tokens;
}
void this.emit(
new AgentStatusUpdated({ agentId: event.agentId, contextTokens: this.state.tokens }),
);
});
this.on(TokenCountingTruncated, (event) => {
const length = normalizeAnchorLength(event.length);
const tokens = Math.max(0, event.tokens);
const anchors = this.state.anchors.filter((a) => a.length <= length);
if (!(this.state.tokens === tokens && anchorsEqual(this.state.anchors, anchors))) {
this.state.anchors = anchors;
this.state.tokens = tokens;
}
void this.emit(
new AgentStatusUpdated({ agentId: event.agentId, contextTokens: this.state.tokens }),
);
});
this.on(TokenCountingRebased, (event) => {
const length = normalizeAnchorLength(event.length);
const tokens = Math.max(0, event.tokens);
const anchors: TokenAnchor[] = [{ length, tokens, measured: event.measured }];
if (!(this.state.tokens === tokens && anchorsEqual(this.state.anchors, anchors))) {
this.state.anchors = anchors;
this.state.tokens = tokens;
}
void this.emit(
new AgentStatusUpdated({ agentId: event.agentId, contextTokens: this.state.tokens }),
);
});
this.on(TokenCountingTurnRecorded, (event) => {
const length = normalizeAnchorLength(event.length);
const tokens = Math.max(0, event.tokens);
const pinned = this.state.anchors.some((anchor) => anchor.length === length);
const anchors = pinned
? this.state.anchors
: [
...this.state.anchors.filter((anchor) => anchor.length < length),
{ length, tokens, measured: false },
];
if (!(this.state.tokens === tokens && anchorsEqual(this.state.anchors, anchors))) {
this.state.anchors = anchors;
this.state.tokens = tokens;
}
void this.emit(
new AgentStatusUpdated({ agentId: event.agentId, contextTokens: this.state.tokens }),
);
});
}
get(start?: number, end?: number): ContextSize {
const context = this.context();
const from = normalizeSliceIndex(start ?? 0, context.length);
const to = normalizeSliceIndex(end ?? context.length, context.length);
const anchor = this.latestAnchor(context.length);
const measuredEnd = Math.min(to, anchor.length);
const estimatedStart = Math.max(from, anchor.length);
const measured =
from === 0 && measuredEnd === anchor.length
? anchor.tokens
: estimateTokensForMessages(context.slice(from, measuredEnd));
const estimated = estimateTokensForMessages(context.slice(estimatedStart, to));
return { size: measured + estimated, measured, estimated };
}
measured(
input: readonly Message[],
_output: readonly Message[],
usage: TokenUsage,
): Promise<void> {
const context = this.context();
if (!matchesContext(input, context)) return Promise.resolve();
return this.emit(
new TokenCountingMeasured({
agentId: this.agent.agentId,
length: context.length,
tokens: tokenUsageTotal(usage),
}),
);
}
latestMeasured(): number {
const anchors = this.state.anchors;
for (let i = anchors.length - 1; i >= 0; i--) {
if (anchors[i]!.measured) return anchors[i]!.tokens;
}
return 0;
}
statusSize(strategy: TokenCountingStrategy): number {
if (strategy === 'measured') return this.latestMeasured();
if (strategy === 'estimated') return estimateTokensForMessages(this.context());
return Math.max(this.get().size, this.latestMeasured());
}
recordTruncation(cutIndex: number): Promise<void> {
if (!this.state.anchors.some((anchor) => anchor.length > cutIndex)) {
return Promise.resolve();
}
return this.emit(
new TokenCountingTruncated({
agentId: this.agent.agentId,
length: cutIndex,
tokens: this.get(0, cutIndex).size,
}),
);
}
rebase(input: TokenCountingRebaseInput): Promise<void> {
return this.emit(
new TokenCountingRebased({
agentId: this.agent.agentId,
length: input.length,
tokens: input.tokens,
measured: input.measured,
}),
);
}
recordTurn(turnId: number, strategy: TokenCountingStrategy): Promise<void> {
return this.emit(
new TokenCountingTurnRecorded({
agentId: this.agent.agentId,
turnId,
length: this.context().length,
tokens: this.statusSize(strategy),
}),
);
}
private context(): readonly ContextMessage[] {
return this.readLegacy(contextMemoryKey) as readonly ContextMessage[];
}
private latestAnchor(contextLength: number): TokenAnchor {
const anchors = this.state.anchors;
for (let i = anchors.length - 1; i >= 0; i--) {
const anchor = anchors[i]!;
if (anchor.length <= contextLength) return anchor;
}
return ZERO_ANCHOR;
}
}
export const TokenCountingAgentModelDefinition = defineAgentModel({
id: 'tokenCounting',
model: TokenCountingAgentModel,
state: {
initial: (): TokenCountingState => ({ anchors: [], tokens: 0 }),
schema: z.custom<TokenCountingState>(),
},
events: [
TokenCountingMeasured,
TokenCountingTruncated,
TokenCountingRebased,
TokenCountingTurnRecorded,
],
});
function matchesContext(input: readonly Message[], context: readonly ContextMessage[]): boolean {
if (input.length !== context.length) return false;
for (let index = 0; index < input.length; index += 1) {
if (input[index] !== context[index]) return false;
}
return true;
}
function tokenUsageTotal(usage: TokenUsage): number {
return usage.inputCacheRead + usage.inputCacheCreation + usage.inputOther + usage.output;
}
function normalizeSliceIndex(index: number, length: number): number {
if (index < 0) return Math.max(length + index, 0);
return Math.min(index, length);
}