@@ -319,8 +319,8 @@ function contentLength(msg: Message): number {
319319 return typeof msg . content === 'string' ? msg . content . length : 0 ;
320320}
321321
322- /** Estimate token count for a single message ( ~3.5 chars/token) . */
323- function estimateTokens ( msg : Message ) : number {
322+ /** Default token counter: ~3.5 chars/token heuristic . */
323+ export function defaultTokenCounter ( msg : Message ) : number {
324324 return Math . ceil ( contentLength ( msg ) / 3.5 ) ;
325325}
326326
@@ -460,6 +460,7 @@ function computeStats(
460460 messagesCompressed : number ,
461461 messagesPreserved : number ,
462462 sourceVersion : number ,
463+ counter : ( msg : Message ) => number ,
463464 messagesDeduped ?: number ,
464465 messagesFuzzyDeduped ?: number ,
465466) : CompressResult [ 'compression' ] {
@@ -468,8 +469,8 @@ function computeStats(
468469 const totalCompressed = messagesCompressed + ( messagesDeduped ?? 0 ) ;
469470 const ratio = compressedTotalChars > 0 ? originalTotalChars / compressedTotalChars : 1 ;
470471
471- const originalTotalTokens = originalMessages . reduce ( ( sum , m ) => sum + estimateTokens ( m ) , 0 ) ;
472- const compressedTotalTokens = resultMessages . reduce ( ( sum , m ) => sum + estimateTokens ( m ) , 0 ) ;
472+ const originalTotalTokens = sumTokens ( originalMessages , counter ) ;
473+ const compressedTotalTokens = sumTokens ( resultMessages , counter ) ;
473474 const tokenRatio = compressedTotalTokens > 0 ? originalTotalTokens / compressedTotalTokens : 1 ;
474475
475476 return {
@@ -492,6 +493,7 @@ function compressSync(
492493 options : CompressOptions = { } ,
493494) : CompressResult {
494495 const sourceVersion = options . sourceVersion ?? 0 ;
496+ const counter = options . tokenCounter ?? defaultTokenCounter ;
495497
496498 if ( messages . length === 0 ) {
497499 return {
@@ -640,7 +642,7 @@ function compressSync(
640642
641643 return {
642644 messages : result ,
643- compression : computeStats ( messages , result , messagesCompressed , messagesPreserved , sourceVersion , messagesDeduped , messagesFuzzyDeduped ) ,
645+ compression : computeStats ( messages , result , messagesCompressed , messagesPreserved , sourceVersion , counter , messagesDeduped , messagesFuzzyDeduped ) ,
644646 verbatim,
645647 } ;
646648}
@@ -664,6 +666,7 @@ async function compressAsync(
664666 options : CompressOptions = { } ,
665667) : Promise < CompressResult > {
666668 const sourceVersion = options . sourceVersion ?? 0 ;
669+ const counter = options . tokenCounter ?? defaultTokenCounter ;
667670 const userSummarizer = options . summarizer ;
668671
669672 if ( messages . length === 0 ) {
@@ -813,7 +816,7 @@ async function compressAsync(
813816
814817 return {
815818 messages : result ,
816- compression : computeStats ( messages , result , messagesCompressed , messagesPreserved , sourceVersion , messagesDeduped , messagesFuzzyDeduped ) ,
819+ compression : computeStats ( messages , result , messagesCompressed , messagesPreserved , sourceVersion , counter , messagesDeduped , messagesFuzzyDeduped ) ,
817820 verbatim,
818821 } ;
819822}
@@ -822,16 +825,17 @@ async function compressAsync(
822825// Token budget helpers (absorbed from compressToFit)
823826// ---------------------------------------------------------------------------
824827
825- function estimateTokensTotal ( messages : Message [ ] ) : number {
826- return messages . reduce ( ( sum , m ) => sum + estimateTokens ( m ) , 0 ) ;
828+ function sumTokens ( messages : Message [ ] , counter : ( msg : Message ) => number ) : number {
829+ return messages . reduce ( ( sum , m ) => sum + counter ( m ) , 0 ) ;
827830}
828831
829832function budgetFastPath (
830833 messages : Message [ ] ,
831834 tokenBudget : number ,
832835 sourceVersion : number ,
836+ counter : ( msg : Message ) => number ,
833837) : CompressResult | undefined {
834- const totalTokens = estimateTokensTotal ( messages ) ;
838+ const totalTokens = sumTokens ( messages , counter ) ;
835839 if ( totalTokens <= tokenBudget ) {
836840 return {
837841 messages,
@@ -851,8 +855,8 @@ function budgetFastPath(
851855 return undefined ;
852856}
853857
854- function addBudgetFields ( cr : CompressResult , tokenBudget : number , recencyWindow : number ) : CompressResult {
855- const tokens = estimateTokensTotal ( cr . messages ) ;
858+ function addBudgetFields ( cr : CompressResult , tokenBudget : number , recencyWindow : number , counter : ( msg : Message ) => number ) : CompressResult {
859+ const tokens = sumTokens ( cr . messages , counter ) ;
856860 return { ...cr , fits : tokens <= tokenBudget , tokenCount : tokens , recencyWindow } ;
857861}
858862
@@ -865,6 +869,7 @@ function forceConvergePass(
865869 tokenBudget : number ,
866870 preserveRoles : Set < string > ,
867871 sourceVersion : number ,
872+ counter : ( msg : Message ) => number ,
868873) : CompressResult {
869874 if ( cr . fits ) return cr ;
870875
@@ -899,7 +904,7 @@ function forceConvergePass(
899904 const truncated = content . slice ( 0 , 512 ) ;
900905 const tag = `[truncated — ${ content . length } chars: ${ truncated } ]` ;
901906
902- const oldTokens = estimateTokens ( m ) ;
907+ const oldTokens = counter ( m ) ;
903908
904909 // If already compressed (has _uc_original), just replace content in-place
905910 const hasOriginal = ! ! ( m . metadata ?. _uc_original ) ;
@@ -922,7 +927,7 @@ function forceConvergePass(
922927 } ;
923928 }
924929
925- const newTokens = estimateTokens ( messages [ cand . idx ] ) ;
930+ const newTokens = counter ( messages [ cand . idx ] ) ;
926931 tokenCount -= ( oldTokens - newTokens ) ;
927932 }
928933
@@ -937,8 +942,9 @@ function compressSyncWithBudget(
937942) : CompressResult {
938943 const minRw = options . minRecencyWindow ?? 0 ;
939944 const sourceVersion = options . sourceVersion ?? 0 ;
945+ const counter = options . tokenCounter ?? defaultTokenCounter ;
940946
941- const fast = budgetFastPath ( messages , tokenBudget , sourceVersion ) ;
947+ const fast = budgetFastPath ( messages , tokenBudget , sourceVersion , counter ) ;
942948 if ( fast ) return fast ;
943949
944950 let lo = minRw ;
@@ -949,7 +955,7 @@ function compressSyncWithBudget(
949955 while ( lo < hi ) {
950956 const mid = Math . ceil ( ( lo + hi ) / 2 ) ;
951957 const cr = compressSync ( messages , { ...options , recencyWindow : mid , summarizer : undefined , tokenBudget : undefined } ) ;
952- lastResult = addBudgetFields ( cr , tokenBudget , mid ) ;
958+ lastResult = addBudgetFields ( cr , tokenBudget , mid , counter ) ;
953959 lastRw = mid ;
954960
955961 if ( lastResult . fits ) {
@@ -964,12 +970,12 @@ function compressSyncWithBudget(
964970 result = lastResult ;
965971 } else {
966972 const cr = compressSync ( messages , { ...options , recencyWindow : lo , summarizer : undefined , tokenBudget : undefined } ) ;
967- result = addBudgetFields ( cr , tokenBudget , lo ) ;
973+ result = addBudgetFields ( cr , tokenBudget , lo , counter ) ;
968974 }
969975
970976 if ( ! result . fits && options . forceConverge ) {
971977 const preserveRoles = new Set ( options . preserve ?? [ 'system' ] ) ;
972- result = forceConvergePass ( result , tokenBudget , preserveRoles , sourceVersion ) ;
978+ result = forceConvergePass ( result , tokenBudget , preserveRoles , sourceVersion , counter ) ;
973979 }
974980
975981 return result ;
@@ -982,8 +988,9 @@ async function compressAsyncWithBudget(
982988) : Promise < CompressResult > {
983989 const minRw = options . minRecencyWindow ?? 0 ;
984990 const sourceVersion = options . sourceVersion ?? 0 ;
991+ const counter = options . tokenCounter ?? defaultTokenCounter ;
985992
986- const fast = budgetFastPath ( messages , tokenBudget , sourceVersion ) ;
993+ const fast = budgetFastPath ( messages , tokenBudget , sourceVersion , counter ) ;
987994 if ( fast ) return fast ;
988995
989996 let lo = minRw ;
@@ -994,7 +1001,7 @@ async function compressAsyncWithBudget(
9941001 while ( lo < hi ) {
9951002 const mid = Math . ceil ( ( lo + hi ) / 2 ) ;
9961003 const cr = await compressAsync ( messages , { ...options , recencyWindow : mid , tokenBudget : undefined } ) ;
997- lastResult = addBudgetFields ( cr , tokenBudget , mid ) ;
1004+ lastResult = addBudgetFields ( cr , tokenBudget , mid , counter ) ;
9981005 lastRw = mid ;
9991006
10001007 if ( lastResult . fits ) {
@@ -1009,12 +1016,12 @@ async function compressAsyncWithBudget(
10091016 result = lastResult ;
10101017 } else {
10111018 const cr = await compressAsync ( messages , { ...options , recencyWindow : lo , tokenBudget : undefined } ) ;
1012- result = addBudgetFields ( cr , tokenBudget , lo ) ;
1019+ result = addBudgetFields ( cr , tokenBudget , lo , counter ) ;
10131020 }
10141021
10151022 if ( ! result . fits && options . forceConverge ) {
10161023 const preserveRoles = new Set ( options . preserve ?? [ 'system' ] ) ;
1017- result = forceConvergePass ( result , tokenBudget , preserveRoles , sourceVersion ) ;
1024+ result = forceConvergePass ( result , tokenBudget , preserveRoles , sourceVersion , counter ) ;
10181025 }
10191026
10201027 return result ;
0 commit comments