fix(token-meter): bound projection state via logged shadow prices
The contextBreakdown and contextPressure units carried the full priced surface, so each session's persisted projection checkpoint grew without bound. A surface replacement is now priced by the shadow-price event logged directly before it — compact/summary for compaction, the new compact/prune from tool-result pruning (priced through the injected token meter) — and the unit states shrink to a fixed handful of numbers. Regenerate the persistence/cordis/module/config catalogs.
This commit is contained in:
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include from '@cordisjs/plugin-include'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
|
||||
let root: string | undefined
|
||||
@@ -23,6 +24,7 @@ describe('compact-tool-result-prune real Loader composition', () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-'))
|
||||
const configPath = join(root, 'cordis.yml')
|
||||
await writeFile(configPath, [
|
||||
"- name: '@deepseek-ai/dsh-token-meter'",
|
||||
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
|
||||
' config:',
|
||||
' thresholdChars: 100',
|
||||
@@ -38,10 +40,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
|
||||
context.loader.internal = {
|
||||
version: 'v2',
|
||||
async import(specifier: string) {
|
||||
if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') {
|
||||
throw new Error(`unexpected Loader import: ${specifier}`)
|
||||
}
|
||||
return ToolResultPruneService
|
||||
if (specifier === '@deepseek-ai/dsh-token-meter') return TokenMeterService
|
||||
if (specifier === '@deepseek-ai/dsh-compact-tool-result-prune') return ToolResultPruneService
|
||||
throw new Error(`unexpected Loader import: ${specifier}`)
|
||||
},
|
||||
} as unknown as NonNullable<typeof context.loader.internal>
|
||||
await context.loader.create({
|
||||
@@ -60,6 +61,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
|
||||
|
||||
it('rejects stale config after plugin schema normalization', async () => {
|
||||
context = new Context()
|
||||
// Satisfy the declared injection first: config normalization runs in the
|
||||
// service constructor, which a pending fiber never reaches.
|
||||
await context.plugin(TokenMeterService)
|
||||
await expect(context.plugin(ToolResultPruneService, {
|
||||
maxChars: 100,
|
||||
} as never)).rejects.toThrow(/unknown key "maxChars"/)
|
||||
|
||||
@@ -9,6 +9,7 @@ import SessionStore, {
|
||||
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
|
||||
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import ToolResultPruneService, {
|
||||
codePointLength,
|
||||
DEFAULTS,
|
||||
@@ -25,9 +26,16 @@ const SMALL: ToolResultPruneConfig = {
|
||||
}
|
||||
|
||||
function service(config: ToolResultPruneConfig = SMALL): ToolResultPruneService {
|
||||
return new ToolResultPruneService(new Context(), config)
|
||||
const ctx = new Context()
|
||||
// Service constructors self-register, so `ctx.tokenMeter` resolves for the
|
||||
// shadow-price pricing without a full plugin boot.
|
||||
void new TokenMeterService(ctx)
|
||||
return new ToolResultPruneService(ctx, config)
|
||||
}
|
||||
|
||||
/** Pricing oracle mirroring the service's estimator for expectations. */
|
||||
const METER = new TokenMeterService(new Context())
|
||||
|
||||
function appendToolStep(
|
||||
session: Session,
|
||||
turn: number,
|
||||
@@ -203,6 +211,18 @@ describe('ToolResultPruneService session transaction', () => {
|
||||
sourceEventSeqs: [originalSeq],
|
||||
})
|
||||
expect(session.surface.nodes).not.toContain(originalSeq)
|
||||
|
||||
// Shadow-price protocol: the metering event sits directly before the
|
||||
// replacement and prices the shadowed node with the shared estimator.
|
||||
if (original.type !== 'tool/result') throw new Error('original is not a tool/result')
|
||||
expect(session.events[entry.replacementSeq - 1]).toMatchObject({
|
||||
type: 'compact/prune',
|
||||
data: {
|
||||
shadowedRange: { start: originalSeq, end: originalSeq },
|
||||
shadowedSeqs: [originalSeq],
|
||||
shadowedTokenCount: METER.estimateMessage(original.data.message),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('prunes multiple results, skips short ones, and converges in one pass', () => {
|
||||
@@ -240,6 +260,7 @@ describe('ToolResultPruneService session transaction', () => {
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(SessionInvariant)
|
||||
await ctx.plugin(TokenMeterService)
|
||||
const prune = new ToolResultPruneService(ctx, SMALL)
|
||||
const session = ctx.sessions.create(SessionId('invariants'))
|
||||
appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }])
|
||||
|
||||
Reference in New Issue
Block a user