Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md # .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.i18n.yaml # .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md # .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md # THIRD_PARTY_NOTICES.md # apps/cli/composition.md # apps/cli/config/base.cordis.yml # apps/cli/package.json # apps/cli/src/app-cli-entry.ts # apps/cli/src/bin.ts # apps/cli/tests/args.spec.ts # apps/web/tests/built-boot.snapshot.ts # apps/web/tests/navigation-panes.e2e.ts # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/llm-streaming.i18n.yaml # docs/event-producer-consumer.md # docs/module-graph.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/bundle/README.i18n.yaml # packages/client/connection/README.i18n.yaml # packages/client/connection/README.md # packages/client/connection/README.zh.md # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/http-bridge.ts # packages/client/connection/src/index.ts # packages/client/connection/tests/fixture.spec.ts # packages/client/connection/tests/node-half.spec.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/contract/session.ts # packages/client/runtime/src/client/sessions/session.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/README.md # packages/client/ui-conversation/README.zh.md # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.module.css # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-conversation/src/client/index.ts # packages/client/ui-conversation/src/client/input/contract.ts # packages/client/ui-conversation/src/client/input/facade.ts # packages/client/ui-conversation/src/client/input/hub.ts # packages/client/ui-conversation/src/client/locales.ts # packages/client/ui-conversation/src/client/service.ts # packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx # packages/client/ui-conversation/src/client/skeleton/InputBar.tsx # packages/client/ui-conversation/tests/apply-inject.spec.tsx # packages/client/ui-conversation/tests/input-bar.spec.tsx # packages/client/ui-conversation/tests/input-matrix.spec.tsx # packages/client/ui-conversation/tests/input-scenarios.spec.tsx # packages/client/ui-conversation/tests/service-orchestration.spec.ts # packages/client/ui-conversation/tests/skeleton.spec.tsx # packages/client/ui-trajectory/tests/views.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/rpc.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/README.md # packages/llm/llm-pi-ai/README.zh.md # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm/README.i18n.yaml # packages/ui/tui/README.md # packages/ui/tui/README.zh.md # packages/ui/tui/src/components/content.ts # packages/ui/tui/src/components/transcript.ts # packages/ui/tui/tests/tui.spec.ts # pnpm-lock.yaml
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// contextBreakdown projection: heuristic system/tools/message composition,
|
||||
// plus the shared estimator's pricing branches.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import type { ContextBreakdownProjection } from '@deepseek-ai/dsh-token-meter/client'
|
||||
import { contextBreakdownProjectionDefinition } from '../src/breakdown-projection.ts'
|
||||
import {
|
||||
estimateContent,
|
||||
estimateHeader,
|
||||
estimateMessage,
|
||||
estimateSystemTokens,
|
||||
estimateToolsTokens,
|
||||
} from '../src/estimate.ts'
|
||||
|
||||
const CONFIG = { provider: 'test', model: 'test-model' }
|
||||
|
||||
const TOOLS: ToolSchema[] = [{
|
||||
name: 'bash',
|
||||
description: 'run a command',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
}]
|
||||
|
||||
async function harness(): Promise<{ ctx: Context; session: Session }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(TokenMeterService)
|
||||
return { ctx, session: ctx.sessions.create() }
|
||||
}
|
||||
|
||||
const projected = (ctx: Context, session: Session): ContextBreakdownProjection => {
|
||||
const value = ctx.sessionProjections.snapshot(session).values.contextBreakdown
|
||||
if (value === undefined) throw new Error('contextBreakdown projection is not registered')
|
||||
return value
|
||||
}
|
||||
|
||||
function appendUser(session: Session, text: string): number {
|
||||
return session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' }).seq
|
||||
}
|
||||
|
||||
/**
|
||||
* Meter one upcoming replacement the way compact-basic does: price the
|
||||
* replaced span from the measurement service's own nodes and log the
|
||||
* shadow-price event directly before the replace.
|
||||
*/
|
||||
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
|
||||
const nodes = ctx.tokenMeter.measure(session).nodes
|
||||
const startIdx = nodes.findIndex(node => node.seq === start)
|
||||
const endIdx = nodes.findIndex(node => node.seq === end)
|
||||
const shadowed = nodes.slice(startIdx, endIdx + 1)
|
||||
session.append('compact/summary', {
|
||||
summary: [{ type: 'text', text: 'summary' }],
|
||||
shadowedRange: { start, end },
|
||||
shadowedSeqs: shadowed.map(node => node.seq),
|
||||
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
}
|
||||
|
||||
describe('contextBreakdown session projection', () => {
|
||||
it('serves zeros for an empty log', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
expect(projected(ctx, session)).toEqual({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 })
|
||||
})
|
||||
|
||||
it('prices the newest envelope last-wins and pushes no change for a restated one', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
session.append('request/header', {
|
||||
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
|
||||
reason: 'initial',
|
||||
})
|
||||
expect(projected(ctx, session)).toEqual({
|
||||
systemTokens: estimateSystemTokens({ config: CONFIG, system: 'You are terse.' }),
|
||||
toolsTokens: estimateToolsTokens({ config: CONFIG, tools: TOOLS }),
|
||||
messageTokens: 0,
|
||||
})
|
||||
|
||||
const changed: string[] = []
|
||||
ctx.sessionProjections.onChanged((_session, key) => { changed.push(key) })
|
||||
session.append('request/header', {
|
||||
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
|
||||
reason: 'change',
|
||||
})
|
||||
session.append('todo/write', { todos: [] })
|
||||
expect(changed).not.toContain('contextBreakdown')
|
||||
|
||||
// A system-less, tool-less envelope prices back to zero.
|
||||
session.append('request/header', { header: { config: CONFIG }, reason: 'change' })
|
||||
expect(projected(ctx, session)).toEqual({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 })
|
||||
})
|
||||
|
||||
it('sums surface appends and skips an empty-content assistant message', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
appendUser(session, 'abcd')
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||
}),
|
||||
usage: { inputTokens: 9, outputTokens: 0 },
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [] })
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
// 'abcd' prices to 9 (1 text + 4 block + 4 role); the usage-only assistant
|
||||
// message derives to no transcript entry and adds nothing.
|
||||
expect(projected(ctx, session).messageTokens).toBe(9)
|
||||
})
|
||||
|
||||
it('shrinks the message figure when a metered replacement compacts the surface', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
const first = appendUser(session, 'before compaction, a longer message')
|
||||
const second = appendUser(session, 'and a second entry')
|
||||
const summary = createUserMessage({
|
||||
content: [{ type: 'text', text: 'summary' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
})
|
||||
appendSummaryMeter(ctx, session, first, second)
|
||||
session.append('user/message', summary, {
|
||||
surfaceOp: { op: 'replace', start: first, end: second },
|
||||
sourceEventSeqs: [first, second],
|
||||
})
|
||||
expect(projected(ctx, session).messageTokens).toBe(estimateMessage(summary))
|
||||
})
|
||||
|
||||
it('keeps the message figure equal to the service surface across appends and a compaction', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
// The panel's composition rows and `measure()` answer the same question in
|
||||
// the same vocabulary; one shared fold is what makes that true.
|
||||
const agree = (): number => {
|
||||
const messageTokens = projected(ctx, session).messageTokens
|
||||
expect(messageTokens).toBe(ctx.tokenMeter.measure(session).surfaceTokens)
|
||||
return messageTokens
|
||||
}
|
||||
session.append('request/header', {
|
||||
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
|
||||
reason: 'initial',
|
||||
})
|
||||
expect(agree()).toBe(0)
|
||||
|
||||
const question = appendUser(session, 'a first question, long enough to price above zero')
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
const answer = session.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'a considered answer' }],
|
||||
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||
}),
|
||||
usage: { inputTokens: 40, outputTokens: 7 },
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [] }).seq
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
const grown = agree()
|
||||
expect(grown).toBeGreaterThan(0)
|
||||
|
||||
appendSummaryMeter(ctx, session, question, answer)
|
||||
// The armed shadow price must not move the published figure by itself.
|
||||
expect(agree()).toBe(grown)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'summary' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: question, end: answer },
|
||||
sourceEventSeqs: [question, answer],
|
||||
})
|
||||
expect(agree()).toBeLessThan(grown)
|
||||
})
|
||||
|
||||
it('folds a replacement without a claim at zero and fails on a mismatched claim', () => {
|
||||
const definition = contextBreakdownProjectionDefinition
|
||||
const replace = (start: number, end: number): SessionEvent => ({
|
||||
type: 'user/message',
|
||||
seq: 9,
|
||||
time: 0,
|
||||
data: createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
|
||||
surfaceOp: { op: 'replace', start, end },
|
||||
sourceEventSeqs: [start, end],
|
||||
} as unknown as SessionEvent)
|
||||
const append = (seq: number): SessionEvent => ({
|
||||
type: 'user/message',
|
||||
seq,
|
||||
time: 0,
|
||||
data: createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
|
||||
surfaceOp: 'append',
|
||||
} as unknown as SessionEvent)
|
||||
const meter = (start: number, end: number, seq: number): SessionEvent => ({
|
||||
type: 'compact/prune',
|
||||
seq,
|
||||
time: 0,
|
||||
data: { shadowedRange: { start, end }, shadowedSeqs: [start, end], shadowedTokenCount: 5 },
|
||||
} as unknown as SessionEvent)
|
||||
let state = definition.init()
|
||||
state = definition.apply(state, append(1))
|
||||
state = definition.apply(state, append(3))
|
||||
// No metering event: the replacement contributes zero instead of throwing.
|
||||
expect(definition.view(definition.apply(state, replace(1, 3))).messageTokens)
|
||||
.toBe(definition.view(state).messageTokens)
|
||||
// An adjacent claim for another range contradicts the replacement.
|
||||
const mismatched = definition.apply(state, meter(1, 1, 8))
|
||||
expect(() => definition.apply(mismatched, replace(1, 3))).toThrow('no adjacent shadow price')
|
||||
// A claim expires after one intervening event, so replacement delta is zero.
|
||||
let expired = definition.apply(state, meter(1, 3, 8))
|
||||
expired = definition.apply(expired, { type: 'todo/write', seq: 9, time: 0, data: { todos: [] } } as unknown as SessionEvent)
|
||||
expect(definition.view(definition.apply(expired, replace(1, 3))).messageTokens)
|
||||
.toBe(definition.view(state).messageTokens)
|
||||
// The armed claim prices exactly the next event's matching replacement.
|
||||
const armed = definition.apply(state, meter(1, 3, 8))
|
||||
expect(definition.view(definition.apply(armed, replace(1, 3))).messageTokens)
|
||||
.toBe(definition.view(state).messageTokens - 5 + estimateMessage(
|
||||
createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
|
||||
))
|
||||
})
|
||||
|
||||
it('keeps the persisted checkpoint O(1) as the surface grows and compacts', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
const first = appendUser(session, 'the first of many messages')
|
||||
for (let index = 0; index < 24; index += 1) appendUser(session, `message number ${index} with some text`)
|
||||
const last = appendUser(session, 'the last message before compaction')
|
||||
const stateKeys = (): string[] => {
|
||||
const row = ctx.sessionProjections.checkpoint(session)['contextBreakdown']
|
||||
if (row === undefined) throw new Error('contextBreakdown checkpoint row is missing')
|
||||
return Object.keys(row.val as Record<string, unknown>).sort()
|
||||
}
|
||||
// Growth adds no per-node bookkeeping to the durable state.
|
||||
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
|
||||
const shadowed = session.surface.nodes.slice(
|
||||
session.surface.nodes.indexOf(first),
|
||||
session.surface.nodes.indexOf(last) + 1,
|
||||
)
|
||||
appendSummaryMeter(ctx, session, first, last)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'summary' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: first, end: last },
|
||||
sourceEventSeqs: [...shadowed],
|
||||
})
|
||||
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
|
||||
expect(projected(ctx, session).messageTokens)
|
||||
.toBe(ctx.tokenMeter.measure(session).surfaceTokens)
|
||||
})
|
||||
|
||||
it('restores from a JSON checkpoint and unregisters with the token-meter fiber', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
const meterFiber = await ctx.plugin(TokenMeterService)
|
||||
const session = ctx.sessions.create()
|
||||
session.append('request/header', {
|
||||
header: { config: CONFIG, system: 'You are terse.' },
|
||||
reason: 'initial',
|
||||
})
|
||||
appendUser(session, 'abcd')
|
||||
const checkpoint = JSON.parse(JSON.stringify(
|
||||
ctx.sessionProjections.checkpoint(session),
|
||||
)) as ReturnType<typeof ctx.sessionProjections.checkpoint>
|
||||
|
||||
await meterFiber.dispose()
|
||||
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextBreakdown')
|
||||
|
||||
await ctx.plugin(TokenMeterService)
|
||||
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextBreakdown).toEqual({
|
||||
systemTokens: estimateSystemTokens({ config: CONFIG, system: 'You are terse.' }),
|
||||
toolsTokens: 0,
|
||||
messageTokens: 9,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shared estimator', () => {
|
||||
it('prices every content-block shape under the fixed heuristic', () => {
|
||||
expect(estimateContent([{ type: 'text', text: 'abcd' }])).toBe(5)
|
||||
expect(estimateContent([{ type: 'reasoning', text: 'abcdefgh' }] as ContentBlock[])).toBe(6)
|
||||
expect(estimateContent([{ type: 'tool-call', id: 'c' as never, name: 'bash', arguments: '{"a":1}' }])).toBe(7)
|
||||
expect(estimateContent([{
|
||||
type: 'tool-result', toolCallId: 'c' as never,
|
||||
content: [{ type: 'text', text: 'abcd' }],
|
||||
}])).toBe(9)
|
||||
const unknown = { type: 'mystery', payload: 'abc' } as unknown as ContentBlock
|
||||
expect(estimateContent([unknown])).toBe(4 + Math.ceil(JSON.stringify(unknown).length / 4))
|
||||
})
|
||||
|
||||
it('prices envelope parts independently and absent parts to zero', () => {
|
||||
expect(estimateSystemTokens(undefined)).toBe(0)
|
||||
expect(estimateSystemTokens({ config: CONFIG })).toBe(0)
|
||||
expect(estimateSystemTokens({ config: CONFIG, system: 'abcdefgh' })).toBe(6)
|
||||
expect(estimateToolsTokens(undefined)).toBe(0)
|
||||
expect(estimateToolsTokens({ config: CONFIG, tools: [] })).toBe(0)
|
||||
expect(estimateToolsTokens({ config: CONFIG, tools: TOOLS }))
|
||||
.toBe(Math.ceil(JSON.stringify(TOOLS).length / 4) + 4)
|
||||
expect(estimateHeader(undefined)).toBe(0)
|
||||
expect(estimateHeader({ config: CONFIG, system: 'abcdefgh', tools: TOOLS }))
|
||||
.toBe(6 + Math.ceil(JSON.stringify(TOOLS).length / 4) + 4)
|
||||
})
|
||||
})
|
||||
@@ -147,7 +147,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('returns a detached deeply immutable empty measurement', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('empty'))
|
||||
const session = Session.create(SessionId('empty'))
|
||||
const result = service.measure(session)
|
||||
expect(result).toEqual({
|
||||
logRevision: 0,
|
||||
@@ -168,7 +168,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('keeps an earlier unified snapshot detached from later replay', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('detached'))
|
||||
const session = Session.create(SessionId('detached'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'first' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -200,7 +200,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('prices header, tools, and surface when no reusable usage exists', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('heuristic'))
|
||||
const session = Session.create(SessionId('heuristic'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -218,7 +218,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('keeps request-header overrides out of the returned surface', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('override-surface'))
|
||||
const session = Session.create(SessionId('override-surface'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -246,7 +246,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('uses disjoint provider usage and signed durable-output rewrites', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('usage'))
|
||||
const session = Session.create(SessionId('usage'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'before' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -267,7 +267,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('selects a heuristic anchor when provider usage would undercut its scale', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('low-usage-anchor'))
|
||||
const session = Session.create(SessionId('low-usage-anchor'))
|
||||
const system = 'system context'
|
||||
const requestHeader = header('deepseek-v4-flash', { system })
|
||||
appendSuccessfulCall(session, requestHeader, {
|
||||
@@ -297,7 +297,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('uses an estimated anchor when provider usage is absent', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('missing-usage'))
|
||||
const session = Session.create(SessionId('missing-usage'))
|
||||
appendSuccessfulCall(session, header('deepseek-v4-flash', { system: 's' }), {
|
||||
providerText: 'provider',
|
||||
durableText: 'rewritten',
|
||||
@@ -314,8 +314,8 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('distinguishes explicit empty provenance from absent legacy provenance', () => {
|
||||
const explicit = new Session(SessionId('explicit-empty'))
|
||||
const legacy = new Session(SessionId('legacy-absent'))
|
||||
const explicit = Session.create(SessionId('explicit-empty'))
|
||||
const legacy = Session.create(SessionId('legacy-absent'))
|
||||
appendSuccessfulCall(explicit, header('deepseek-v4-flash'), {
|
||||
durableText: 'listener injected text',
|
||||
providerText: '',
|
||||
@@ -335,7 +335,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('keeps only the latest successful request anchor across model switches', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('switch'))
|
||||
const session = Session.create(SessionId('switch'))
|
||||
const alphaHeader = header('alpha', { system: 'same envelope' })
|
||||
appendSuccessfulCall(session, alphaHeader, { usage: USAGE, providerText: 'alpha' })
|
||||
expect(service.measure(session).baseline).toMatchObject({ kind: 'usage', tokens: 34 })
|
||||
@@ -356,7 +356,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('invalidates usage for any canonical envelope change or explicit override', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('envelope'))
|
||||
const session = Session.create(SessionId('envelope'))
|
||||
const anchoredHeader = header('deepseek-v4-flash', { system: 'one' })
|
||||
appendSuccessfulCall(session, anchoredHeader, { usage: USAGE })
|
||||
expect(service.measure(session, { ...anchoredHeader, tools: [] }).baseline.kind).toBe('usage')
|
||||
@@ -375,7 +375,7 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('folds the latest full header snapshot into the effective envelope', () => {
|
||||
const session = new Session(SessionId('header-snapshot'))
|
||||
const session = Session.create(SessionId('header-snapshot'))
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
session.append('request/header', {
|
||||
header: header('deepseek-v4-pro'),
|
||||
@@ -388,7 +388,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('replays seeded append and replace operations with signed deltas', () => {
|
||||
const service = meter()
|
||||
const original = new Session(SessionId('surface-original'))
|
||||
const original = Session.create(SessionId('surface-original'))
|
||||
appendSuccessfulCall(original, header('deepseek-v4-flash'), {
|
||||
usage: USAGE,
|
||||
providerText: 'long provider answer '.repeat(100),
|
||||
@@ -397,7 +397,7 @@ describe('replay anchors and surface folds', () => {
|
||||
content: [{ type: 'text', text: 'new tail' }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
const seeded = new Session(SessionId('surface-seeded'), original.events)
|
||||
const seeded = Session.create(SessionId('surface-seeded'), original.events)
|
||||
const before = service.measure(seeded)
|
||||
expect(before.nodes).toHaveLength(2)
|
||||
expect(before.surfaceDeltaTokens).toBeGreaterThan(0)
|
||||
@@ -423,7 +423,7 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('prices an empty assistant surface anchor as zero', () => {
|
||||
const session = new Session(SessionId('empty-assistant'))
|
||||
const session = Session.create(SessionId('empty-assistant'))
|
||||
appendSuccessfulCall(session, header('deepseek-v4-flash'), {
|
||||
providerText: '',
|
||||
durableText: '',
|
||||
@@ -444,7 +444,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
}
|
||||
|
||||
it('rejects an assistant without its step boundary transactionally', () => {
|
||||
const session = new Session(SessionId('bad-step'))
|
||||
const session = Session.create(SessionId('bad-step'))
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
@@ -462,7 +462,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('clears completed step boundaries and rejects overlapping or late step events', () => {
|
||||
const overlapping = new Session(SessionId('overlapping-step'))
|
||||
const overlapping = Session.create(SessionId('overlapping-step'))
|
||||
overlapping.append('step/start', { turn: 1, step: 1 })
|
||||
overlapping.append('step/start', { turn: 1, step: 2 })
|
||||
expectRepeatedFailure(
|
||||
@@ -471,7 +471,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
/arrived before turn 1\/step 1 ended/,
|
||||
)
|
||||
|
||||
const late = new Session(SessionId('late-assistant'))
|
||||
const late = Session.create(SessionId('late-assistant'))
|
||||
late.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(late, header('deepseek-v4-flash'))
|
||||
late.append('step/end', { turn: 1, step: 1 })
|
||||
@@ -493,7 +493,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
/no matching step\/start/,
|
||||
)
|
||||
|
||||
const mismatchedEnd = new Session(SessionId('mismatched-end'))
|
||||
const mismatchedEnd = Session.create(SessionId('mismatched-end'))
|
||||
mismatchedEnd.append('step/start', { turn: 1, step: 1 })
|
||||
mismatchedEnd.append('step/end', { turn: 1, step: 2 })
|
||||
expectRepeatedFailure(
|
||||
@@ -532,7 +532,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
},
|
||||
]
|
||||
for (const testCase of cases) {
|
||||
const session = new Session(SessionId(`bad-source-${testCase.name}`))
|
||||
const session = Session.create(SessionId(`bad-source-${testCase.name}`))
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
const sourceEventSeqs = testCase.appendSource(session)
|
||||
@@ -554,7 +554,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('rejects repeated and non-earlier assistant provenance', () => {
|
||||
const duplicate = new Session(SessionId('duplicate-source'))
|
||||
const duplicate = Session.create(SessionId('duplicate-source'))
|
||||
duplicate.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(duplicate, header('deepseek-v4-flash'))
|
||||
const source = duplicate.append('assistant/chunk', {
|
||||
@@ -584,7 +584,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
expect(() => meter().measure(duplicate)).toThrow(/repeats source seq/)
|
||||
|
||||
const future = new Session(SessionId('future-source'))
|
||||
const future = Session.create(SessionId('future-source'))
|
||||
future.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(future, header('deepseek-v4-flash'))
|
||||
appendUnchecked(future, {
|
||||
@@ -611,7 +611,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('does not partially apply a malformed assistant replacement', () => {
|
||||
const session = new Session(SessionId('transactional-replace'))
|
||||
const session = Session.create(SessionId('transactional-replace'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -638,7 +638,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('rejects corrupt replacement ranges without advancing the replay cursor', () => {
|
||||
const session = new Session(SessionId('bad-replace'))
|
||||
const session = Session.create(SessionId('bad-replace'))
|
||||
const head = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -671,7 +671,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
type: 'turn/start',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
data: { turn: 1 },
|
||||
}] })
|
||||
activeMeter.measure(session)
|
||||
session.append('user/message', createUserMessage({
|
||||
|
||||
@@ -70,6 +70,26 @@ const projected = (ctx: Context, session: Session): TokenUsageProjection => {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Meter one upcoming replacement the way compact-basic does: price the
|
||||
* replaced span from the measurement service's own nodes and log the
|
||||
* shadow-price event directly before the replace.
|
||||
*/
|
||||
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
|
||||
const nodes = ctx.tokenMeter.measure(session).nodes
|
||||
const startIdx = nodes.findIndex(node => node.seq === start)
|
||||
const endIdx = nodes.findIndex(node => node.seq === end)
|
||||
const shadowed = nodes.slice(startIdx, endIdx + 1)
|
||||
session.append('compact/summary', {
|
||||
summary: [{ type: 'text', text: 'summary' }],
|
||||
shadowedRange: { start, end },
|
||||
shadowedSeqs: shadowed.map(node => node.seq),
|
||||
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
}
|
||||
|
||||
describe('tokenUsage session projection', () => {
|
||||
it('serves zero buckets for an empty log', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
@@ -184,6 +204,7 @@ describe('tokenUsage session projection', () => {
|
||||
content: [{ type: 'text', text: 'before compaction' }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
appendSummaryMeter(ctx, session, before.seq, before.seq)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'compacted' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
@@ -235,6 +256,34 @@ function recordContext(session: Session, model: string, contextWindow?: number):
|
||||
})
|
||||
}
|
||||
|
||||
/** Append one model-visible user turn and return its surface seq. */
|
||||
function appendUser(session: Session, text: string): number {
|
||||
return session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' }).seq
|
||||
}
|
||||
|
||||
/** Append one finalized assistant turn carrying its provider usage. */
|
||||
function appendAssistant(
|
||||
session: Session,
|
||||
text: string,
|
||||
usage: TokenUsage,
|
||||
turn: number,
|
||||
step: number,
|
||||
): number {
|
||||
return session.append('assistant/message', {
|
||||
turn,
|
||||
step,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||
}),
|
||||
usage,
|
||||
}, { surfaceOp: 'append', sourceEventSeqs: [] }).seq
|
||||
}
|
||||
|
||||
describe('contextPressure session projection', () => {
|
||||
it('serves no pressure or capacity for an empty log', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
@@ -277,9 +326,13 @@ describe('contextPressure session projection', () => {
|
||||
startStep(session, 1, 1)
|
||||
recordContext(session, 'small', 64_000)
|
||||
usageChunk(session, { inputTokens: 100, outputTokens: 10 }, 1, 1)
|
||||
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, contextWindow: 64_000 })
|
||||
expect(pressure(ctx, session)).toEqual({
|
||||
pressureTokens: 100, projectedTokens: 100, contextWindow: 64_000,
|
||||
})
|
||||
recordContext(session, 'large', 256_000)
|
||||
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, contextWindow: 256_000 })
|
||||
expect(pressure(ctx, session)).toEqual({
|
||||
pressureTokens: 100, projectedTokens: 100, contextWindow: 256_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('removes an older capacity when the newest route advertises none', async () => {
|
||||
@@ -288,7 +341,7 @@ describe('contextPressure session projection', () => {
|
||||
recordContext(session, 'small', 64_000)
|
||||
usageChunk(session, { inputTokens: 100, outputTokens: 10 }, 1, 1)
|
||||
recordContext(session, 'unknown')
|
||||
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100 })
|
||||
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, projectedTokens: 100 })
|
||||
})
|
||||
|
||||
it('pushes no change for unrelated events or a restated capacity', async () => {
|
||||
@@ -319,7 +372,7 @@ describe('contextPressure session projection', () => {
|
||||
const checkpoint = JSON.parse(JSON.stringify(
|
||||
ctx.sessionProjections.checkpoint(session),
|
||||
)) as ReturnType<typeof ctx.sessionProjections.checkpoint>
|
||||
expect(checkpoint.contextPressure?.ver).toBe(2)
|
||||
expect(checkpoint.contextPressure?.ver).toBe(4)
|
||||
|
||||
await meterFiber.dispose()
|
||||
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextPressure')
|
||||
@@ -327,7 +380,81 @@ describe('contextPressure session projection', () => {
|
||||
await ctx.plugin(TokenMeterService)
|
||||
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextPressure).toEqual({
|
||||
pressureTokens: 42,
|
||||
projectedTokens: 42,
|
||||
contextWindow: 64_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('carries the sample forward over surface growth and a compaction', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
recordContext(session, 'large', 128_000)
|
||||
const question = appendUser(session, 'a first question worth a few tokens')
|
||||
startStep(session, 1, 1)
|
||||
// The provider prices the prompt its request actually carried; the sample
|
||||
// must anchor against the surface as of that request, not after the
|
||||
// assistant message joins it.
|
||||
const answer = appendAssistant(session, 'an answer of some length', { inputTokens: 900, outputTokens: 20 }, 1, 1)
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
const afterTurn = pressure(ctx, session)
|
||||
expect(afterTurn.pressureTokens).toBe(900)
|
||||
// The assistant message landed after the sample, so it already shows.
|
||||
expect(afterTurn.projectedTokens).toBeGreaterThan(900)
|
||||
|
||||
const grown = appendUser(session, 'a follow-up question that grows the surface further')
|
||||
const beforeCompaction = pressure(ctx, session).projectedTokens
|
||||
expect(beforeCompaction).toBeGreaterThan(afterTurn.projectedTokens!)
|
||||
|
||||
// Compaction reports no usage of its own, so `pressureTokens` cannot move;
|
||||
// the projected figure must shrink anyway — the defect this field fixes.
|
||||
appendSummaryMeter(ctx, session, question, grown)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'summary' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: question, end: grown },
|
||||
sourceEventSeqs: [question, answer, grown],
|
||||
})
|
||||
const compacted = pressure(ctx, session)
|
||||
expect(compacted.pressureTokens).toBe(900)
|
||||
expect(compacted.projectedTokens).toBeLessThan(beforeCompaction!)
|
||||
})
|
||||
|
||||
it('folds a replacement without a claim at zero', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
const question = appendUser(session, 'a question from an unmetered log')
|
||||
startStep(session, 1, 1)
|
||||
usageChunk(session, { inputTokens: 100, outputTokens: 1 }, 1, 1)
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
const before = pressure(ctx, session)
|
||||
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'summary without a preceding claim' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: question, end: question },
|
||||
sourceEventSeqs: [question],
|
||||
})
|
||||
|
||||
expect(pressure(ctx, session)).toEqual(before)
|
||||
})
|
||||
|
||||
it('clamps a projection that heuristic error drove below zero', async () => {
|
||||
const { ctx, session } = await harness()
|
||||
recordContext(session, 'large', 128_000)
|
||||
const question = appendUser(session, 'a question long enough to outprice the sample'.repeat(4))
|
||||
startStep(session, 1, 1)
|
||||
// A provider sample far below the heuristic price of what it replaced:
|
||||
// shadowing that span subtracts more than the sample holds.
|
||||
appendAssistant(session, 'ok', { inputTokens: 3, outputTokens: 1 }, 1, 1)
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
appendSummaryMeter(ctx, session, question, question)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: '.' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: question, end: question },
|
||||
sourceEventSeqs: [question],
|
||||
})
|
||||
expect(pressure(ctx, session).projectedTokens).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user