import { Disposable } from '#/_base/di/lifecycle'; import type { AgentContext } from '#/agent/agentContext/agentContext'; import { agentSpaceOf } from '#/agent/agentContext/agentSpace'; import { TurnEnded } from '#/agent/loop/turnOps'; import { IConfigService } from '#/app/config/config'; import { ISessionEventBus } from '#/app/event/eventBus'; import { TOKEN_COUNTING_SECTION, type TokenCountingConfig, } from '#/agent/tokenCounting/configSection'; import type { ContextSize, TokenCountingRequest, TokenCountingStrategy, } from '#/agent/tokenCounting/tokenCounting'; import type { Message } from '#/llm-adapter/contract/message'; import type { ToolDescription as Tool } from '#human/llm/message'; import { estimateTokens, estimateTokensForMessage, estimateTokensForMessages, estimateTokensForTools, } from '#/llm-adapter/contract/tokens'; import type { TokenUsage } from '#human/llm/usage'; import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; import { ISessionTokenCountingService, type TokenCountingRebaseInput, } from './sessionTokenCounting'; import { TokenCountingAgentModelDefinition } from './tokenCountingAgentModel'; export class SessionTokenCountingService extends Disposable implements ISessionTokenCountingService { declare readonly _serviceBrand: undefined; constructor( @IConfigService private readonly config: IConfigService, @ISessionEventBus eventBus: ISessionEventBus, @IAgentLifecycleService agentLifecycle: IAgentLifecycleService, ) { super(); this._register( eventBus.subscribe(TurnEnded, (event) => { const agent = agentLifecycle.get(event.agentId); if (agent === undefined) return; void agentSpaceOf(agent).use( TokenCountingAgentModelDefinition, (model) => model.recordTurn(event.turnId, this.strategy), ); }), ); } get strategy(): TokenCountingStrategy { return ( this.config.get(TOKEN_COUNTING_SECTION)?.strategy ?? 'measured+estimated' ); } get(agent: AgentContext, start?: number, end?: number): ContextSize { return agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.get(start, end), ); } measured( agent: AgentContext, input: readonly Message[], output: readonly Message[], usage: TokenUsage, ): void { void agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.measured(input, output, usage), ); } latestMeasured(agent: AgentContext): number { return agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.latestMeasured(), ); } statusSize(agent: AgentContext): number { return agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.statusSize(this.strategy), ); } recordTruncation(agent: AgentContext, cutIndex: number): void { void agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.recordTruncation(cutIndex), ); } rebase(agent: AgentContext, input: TokenCountingRebaseInput): void { void agentSpaceOf(agent).use(TokenCountingAgentModelDefinition, (model) => model.rebase(input), ); } requestSize(request: TokenCountingRequest): number { return ( this.estimateText(request.systemPrompt) + this.estimateTools(request.tools) + this.estimateMessages(request.messages) ); } estimateText(text: string): number { return estimateTokens(text); } estimateMessage(message: Message): number { return estimateTokensForMessage(message); } estimateMessages(messages: readonly Message[]): number { return estimateTokensForMessages(messages); } estimateTools(tools: readonly Tool[]): number { return estimateTokensForTools(tools); } }