fix(token-meter): close projected usage review gaps

This commit is contained in:
Hypatia May
2026-07-30 17:22:15 +08:00
parent e23cd8e406
commit 6d58953f30
43 changed files with 280 additions and 183 deletions
+14 -14
View File
@@ -44,7 +44,7 @@ import {
} from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message, PreparedLlmCall, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
import type { AssistantMessage, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
import type { AssistantMessage, RequestContext, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tools'
import { executeToolCalls } from './tool-calls.ts'
@@ -668,21 +668,21 @@ export class ReactLoopAgent implements Agent {
session.append('request/header', { header, reason: 'change' })
}
// Capacity of the route this request resolved to, recorded from the same
// Context metadata for the route this request resolved to, recorded from the same
// registration-bound lookup that prepared the call (no second resolve).
// Deduplicated against the last record: an unchanged route logs nothing.
// A route with unknown capacity is still recorded so it clears any older
// denominator; an unchanged route logs nothing.
const contextWindow = preparedCall?.context?.contextWindow
if (contextWindow !== undefined) {
const previous = session.requestContext()
if (previous?.provider !== config.provider
|| previous.model !== config.model
|| previous.contextWindow !== contextWindow) {
session.append('request/context', {
provider: config.provider,
model: config.model,
contextWindow,
})
}
const requestContext: RequestContext = {
provider: config.provider,
model: config.model,
...contextWindow === undefined ? {} : { contextWindow },
}
const previous = session.requestContext()
if (previous?.provider !== requestContext.provider
|| previous.model !== requestContext.model
|| previous.contextWindow !== requestContext.contextWindow) {
session.append('request/context', requestContext)
}
const request = markAgentLoopRequest(deepFreeze({
@@ -578,13 +578,38 @@ describe('request/context capacity records', () => {
.map(event => event.data.contextWindow)).toEqual([64_000, 256_000])
})
it('records nothing when the adapter advertises no capacity', async () => {
// The absent-capacity path must stay silent rather than log a placeholder:
// consumers read "no capacity known" and omit their percentage entirely.
const ctx = await harness(new MockAdapter([textResponse('a')]))
it('records and deduplicates a route whose adapter advertises no capacity', async () => {
const ctx = await harness(new MockAdapter([textResponse('a'), textResponse('b')]))
const agent = ctx.agentLoop.create(SessionId('capacity-absent'), { provider: 'mock', model: 'mock' })
send(agent, 'go')
send(agent, 'first')
await waitForIdle(ctx, agent)
expect(agent.session.events.some(event => event.type === 'request/context')).toBe(false)
send(agent, 'second')
await waitForIdle(ctx, agent)
expect(agent.session.events
.filter(event => event.type === 'request/context')
.map(event => event.data)).toEqual([{ provider: 'mock', model: 'mock' }])
})
it('clears a previous capacity when the next route advertises none', async () => {
const adapter = capacityAdapter({ known: 64_000 }, [textResponse('a'), textResponse('b')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('capacity-clear'), { provider: 'mock', model: 'known' })
let model = 'known'
ctx.on('agent/request', (subject, _turn, _step, _signal, next) => subject === agent
? Promise.resolve({ provider: 'mock', model })
: next())
send(agent, 'first')
await waitForIdle(ctx, agent)
model = 'unknown'
send(agent, 'second')
await waitForIdle(ctx, agent)
expect(agent.session.events
.filter(event => event.type === 'request/context')
.map(event => event.data)).toEqual([
{ provider: 'mock', model: 'known', contextWindow: 64_000 },
{ provider: 'mock', model: 'unknown' },
])
})
})
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/session/README.md
README.md: 861b96c453e677807bded2475fe8e62a74bcd299
README.zh.md: 6974a072f5cb32f4e850846bbb02af59cda93303
README.md: fe96b5c9735d48d4f92210970b7707749a920787
README.zh.md: 6f8aaeef2464a946e10aeef0cfe13b36ab303aeb
+1 -1
View File
@@ -65,7 +65,7 @@ Providers stream token-sized deltas, so a raw log stores hundreds of `assistant/
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`request/context` records the registration-bound `contextWindow` of the route a request resolved to, appended inside its step beside `request/header` and only when the provider, model, or capacity differs from the previous record. `session.requestContext()` folds the latest one incrementally, mirroring `requestHeader()`. Capacity stays OUT of `EpochHeader` on purpose: it is adapter metadata describing a route, not an input the request was built from, so it must not enter request reconstruction or header equality — a capacity change is not a header `change`. A route whose adapter advertises no capacity appends nothing.
`request/context` records registration-bound metadata for the route a request resolved to, appended inside its step beside `request/header` and only when the provider, model, or capacity differs from the previous record. `session.requestContext()` folds the latest one incrementally, mirroring `requestHeader()`. Capacity stays OUT of `EpochHeader` on purpose: it is adapter metadata describing a route, not an input the request was built from, so it must not enter request reconstruction or header equality — a capacity change is not a header `change`. A route whose adapter advertises no capacity is still recorded with `contextWindow` absent, clearing any older known capacity.
A `user/message` stores the complete `UserMessage` directly, including the identity created before routing or prompt admission. It renders its `content` verbatim whether it is a direct human prompt, a synthetic injection, or an admitted goal round; its typed `source` is the only channel that tells them apart and carries any domain-specific durable facts. `assistant/message`, `tool/result`, and `steering/message` likewise store complete message values. Turn execution remains enclosed by `turn/start` and `turn/end`, while an idle injection may append and flush a `user/message` between turns without running the model.
+1 -1
View File
@@ -65,7 +65,7 @@
`request/header` 记录非历史请求封装的完整规范快照,其原因为 `initial`、`resume` 或 `change`。`foldRequestHeader()` 选择最新快照;旧版增量事件和已移除的 `fallback` 原因会被拒绝。详见[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
`request/context` 记录请求所解析到的路由的、绑定注册项的 `contextWindow`,在其所属步骤内紧随 `request/header` 追加,且仅在提供方、模型或容量与上一条记录不同时追加。`session.requestContext()` 以增量方式归并最新一条,与 `requestHeader()` 保持一致。容量刻意不进入 `EpochHeader`:它是描述路由的适配器元数据,不是构建该请求所依据的输入,因此绝不可进入请求重建或请求头相等性判断:容量变化不构成请求头 `change`。适配器不公布容量的路由不追加任何记录。
`request/context` 记录请求所解析到的路由的、绑定注册项的元数据,在其所属步骤内紧随 `request/header` 追加,且仅在提供方、模型或容量与上一条记录不同时追加。`session.requestContext()` 以增量方式归并最新一条,与 `requestHeader()` 保持一致。容量刻意不进入 `EpochHeader`:它是描述路由的适配器元数据,不是构建该请求所依据的输入,因此绝不可进入请求重建或请求头相等性判断:容量变化不构成请求头 `change`。适配器不公布容量的路由仍会被记录,但 `contextWindow` 字段缺失,从而清除较早的已知容量。
`user/message` 会直接存储完整的 `UserMessage`,其中包括路由或提示词准入前创建的标识。无论它是直接人类提示词、合成注入,还是已准入的 Goal Round,都会原样呈现其 `content`;带类型的 `source` 是区分三者的唯一通道,并携带各领域专有的持久事实。`assistant/message`、`tool/result` 和 steering(中途引导)对应的 `steering/message` 也会存储完整的消息值。轮次执行仍由 `turn/start` 与 `turn/end` 包围,而空闲注入可以在轮次之间追加并刷新一条 `user/message`,无需运行模型。
+2 -2
View File
@@ -584,11 +584,11 @@ export class Session {
private contextFoldSeq = 0
/**
* The route capacity in force after the log's last `request/context` event —
* The route metadata in force after the log's last `request/context` event —
* what the NEXT request deduplicates against — or undefined before any such
* record. Maintained incrementally like {@link requestHeader}, so a per-step
* read costs O(new events).
* @returns the folded capacity record, or undefined when none exists yet.
* @returns the folded context record, or undefined when none exists yet.
*/
requestContext(): RequestContext | undefined {
if (this.contextFoldSeq < this.log.length) {
+8 -8
View File
@@ -170,17 +170,17 @@ export interface EpochHeader {
}
/**
* Registration-bound context capacity of one resolved model route. Adapter
* Registration-bound context metadata of one resolved model route. Adapter
* metadata about a route rather than a request input, which is why it lives
* outside {@link EpochHeader}.
*/
export interface RequestContext {
/** Registered provider route the capacity was resolved through. */
/** Registered provider route the metadata was resolved through. */
provider: string
/** Provider-owned model id the capacity belongs to. */
/** Provider-owned model id the metadata belongs to. */
model: string
/** Maximum combined request and response context in tokens. */
contextWindow: number
/** Maximum combined request and response context in tokens; absent when the adapter advertises none. */
contextWindow?: number
}
/**
@@ -265,13 +265,13 @@ export interface SessionEventMap {
*/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
/**
* Registration-bound context capacity for the route a request resolved to,
* Registration-bound context metadata for the route a request resolved to,
* appended inside its step beside `request/header` and only when the route
* or capacity differs from the last record. It is log-only and deliberately
* NOT part of {@link EpochHeader}: capacity is adapter metadata about a
* route, not an input the request was built from, so it must not participate
* in request reconstruction or header equality. Absent for a route whose
* adapter advertises no capacity.
* in request reconstruction or header equality. `contextWindow` is absent
* when the route's adapter advertises no capacity.
*/
'request/context': RequestContext
/**
@@ -96,7 +96,7 @@ describe('Session.requestContext', () => {
const CAPACITY = { provider: 'mock', model: 'm', contextWindow: 128_000 }
/** A turn-enclosed capacity record; the invariant rejects one outside a turn. */
function seedWith(...records: { provider: string; model: string; contextWindow: number }[]): SessionEvent[] {
function seedWith(...records: { provider: string; model: string; contextWindow?: number }[]): SessionEvent[] {
const events: SessionEvent[] = [{
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
}]
@@ -127,6 +127,8 @@ describe('Session.requestContext', () => {
expect(session.requestContext()).toEqual(CAPACITY)
session.append('request/context', { ...CAPACITY, model: 'next', contextWindow: 64_000 })
expect(session.requestContext()).toEqual({ provider: 'mock', model: 'next', contextWindow: 64_000 })
session.append('request/context', { provider: 'mock', model: 'unknown' })
expect(session.requestContext()).toEqual({ provider: 'mock', model: 'unknown' })
})
it('folds a batch appended between two reads', () => {
@@ -143,6 +145,6 @@ describe('Session.requestContext', () => {
const held = session.requestContext()
if (held === undefined) throw new Error('expected a folded capacity record')
expect(Object.isFrozen(held)).toBe(true)
expect(() => { (held as { contextWindow: number }).contextWindow = 1 }).toThrow()
expect(() => { (held as { contextWindow?: number }).contextWindow = 1 }).toThrow()
})
})