@@ -167,13 +167,15 @@ func (b ReplanPromptBudgeter) compactRecentResults(results []WorkerTurnResult, b
167167 }
168168 recent = append (recent , b .compactWorkerResult (result ))
169169 }
170- for approxJSONTokens (recent ) > b .RecentResultsTokens {
170+ recentTokens := newJSONArrayTokenSizer (recent )
171+ for recentTokens .tokens () > b .RecentResultsTokens {
171172 dropped := false
172173 for i , result := range recent {
173174 if isHighPriorityPromptResult (result , blocked ) {
174175 continue
175176 }
176177 recent = append (recent [:i ], recent [i + 1 :]... )
178+ recentTokens .drop (i )
177179 dropped = true
178180 break
179181 }
@@ -216,7 +218,8 @@ func (b ReplanPromptBudgeter) compactWorkerResult(result WorkerTurnResult) Worke
216218
217219func (b ReplanPromptBudgeter ) compactContextLedger (entries []ContextLedgerEntry ) []ContextLedgerEntry {
218220 compact := compactContextLedgerForPrompt (entries )
219- for approxJSONTokens (compact ) > b .ContextLedgerTokens && len (compact ) > 0 {
221+ compactTokens := newJSONArrayTokenSizer (compact )
222+ for compactTokens .tokens () > b .ContextLedgerTokens && len (compact ) > 0 {
220223 dropIndex := - 1
221224 for i , entry := range compact {
222225 if entry .Error == "" && ! strings .Contains (entry .Kind , "candidate" ) {
@@ -228,6 +231,7 @@ func (b ReplanPromptBudgeter) compactContextLedger(entries []ContextLedgerEntry)
228231 dropIndex = 0
229232 }
230233 compact = append (compact [:dropIndex ], compact [dropIndex + 1 :]... )
234+ compactTokens .drop (dropIndex )
231235 }
232236 return compact
233237}
@@ -237,10 +241,13 @@ func (b ReplanPromptBudgeter) compactPullRequestFeedback(items []PullRequestFeed
237241 for i := range compact {
238242 compact [i ].Prompt = truncateStringForPrompt (compact [i ].Prompt , tokensToApproxChars (1000 ))
239243 }
240- for approxJSONTokens (compact ) > b .PullRequestFeedbackTokens && len (compact ) > 0 {
244+ compactTokens := newJSONArrayTokenSizer (compact )
245+ for compactTokens .tokens () > b .PullRequestFeedbackTokens && len (compact ) > 0 {
241246 compact [0 ].Prompt = truncateStringForPrompt (compact [0 ].Prompt , tokensToApproxChars (250 ))
242- if approxJSONTokens (compact ) > b .PullRequestFeedbackTokens {
247+ compactTokens .update (0 , compact [0 ])
248+ if compactTokens .tokens () > b .PullRequestFeedbackTokens {
243249 compact = compact [1 :]
250+ compactTokens .drop (0 )
244251 }
245252 }
246253 return compact
@@ -259,7 +266,8 @@ func (b ReplanPromptBudgeter) compactArtifacts(artifacts []core.TaskArtifact) []
259266 }
260267 compact = append (compact , item )
261268 }
262- for approxJSONTokens (compact ) > b .ArtifactsTokens && len (compact ) > 0 {
269+ compactTokens := newJSONArrayTokenSizer (compact )
270+ for compactTokens .tokens () > b .ArtifactsTokens && len (compact ) > 0 {
263271 dropIndex := 0
264272 for i , artifact := range compact {
265273 if artifact .Kind != "worker_result_digest" {
@@ -268,12 +276,14 @@ func (b ReplanPromptBudgeter) compactArtifacts(artifacts []core.TaskArtifact) []
268276 }
269277 }
270278 compact = append (compact [:dropIndex ], compact [dropIndex + 1 :]... )
279+ compactTokens .drop (dropIndex )
271280 }
272281 return compact
273282}
274283
275284func (b ReplanPromptBudgeter ) degradeToTotalBudget (payload map [string ]any , state ReplanPromptState ) ReplanPromptState {
276- for approxJSONTokens (payload ) > b .TotalTokens {
285+ currentTokens := approxJSONTokens (payload )
286+ for currentTokens > b .TotalTokens {
277287 switch {
278288 case len (state .Artifacts ) > 0 :
279289 state .Artifacts = state .Artifacts [1 :]
@@ -288,16 +298,18 @@ func (b ReplanPromptBudgeter) degradeToTotalBudget(payload map[string]any, state
288298 state .RecentResults [i ].Changes .ChangedFiles = nil
289299 }
290300 state .RecoveryHint = truncateStringForPrompt (state .RecoveryHint , tokensToApproxChars (250 ))
301+ payload ["state" ] = state
291302 state .PromptBudget .ApproxTokens = approxJSONTokens (payload )
292303 return state
293304 }
294- state .PromptBudget .ApproxTokens = approxJSONTokens (state )
295305 state .PromptBudget .RecentResultCount = len (state .RecentResults )
296306 state .PromptBudget .ArtifactCount = len (state .Artifacts )
297307 state .PromptBudget .ContextLedgerCount = len (state .ContextLedger )
298308 payload ["state" ] = state
309+ currentTokens = approxJSONTokens (payload )
310+ state .PromptBudget .ApproxTokens = currentTokens
299311 }
300- state .PromptBudget .ApproxTokens = approxJSONTokens ( payload )
312+ state .PromptBudget .ApproxTokens = currentTokens
301313 return state
302314}
303315
@@ -317,8 +329,10 @@ func compactTaskSteeringForPrompt(items []string) []string {
317329 for i := range compact {
318330 compact [i ] = truncateStringForPrompt (compact [i ], tokensToApproxChars (1000 ))
319331 }
320- for approxJSONTokens (compact ) > 4000 && len (compact ) > 1 {
332+ compactTokens := newJSONArrayTokenSizer (compact )
333+ for compactTokens .tokens () > 4000 && len (compact ) > 1 {
321334 compact = compact [1 :]
335+ compactTokens .drop (0 )
322336 }
323337 return compact
324338}
@@ -404,11 +418,76 @@ func approxJSONTokens(value any) int {
404418 return approxTokensForString (string (data ))
405419}
406420
421+ type jsonArrayTokenSizer [T any ] struct {
422+ nilSlice bool
423+ itemBytes []int
424+ }
425+
426+ func newJSONArrayTokenSizer [T any ](items []T ) * jsonArrayTokenSizer [T ] {
427+ sizer := & jsonArrayTokenSizer [T ]{
428+ nilSlice : items == nil ,
429+ itemBytes : make ([]int , 0 , len (items )),
430+ }
431+ for _ , item := range items {
432+ sizer .itemBytes = append (sizer .itemBytes , jsonEncodedBytes (item ))
433+ }
434+ return sizer
435+ }
436+
437+ func (s * jsonArrayTokenSizer [T ]) tokens () int {
438+ return approxTokensForBytes (s .bytes ())
439+ }
440+
441+ func (s * jsonArrayTokenSizer [T ]) bytes () int {
442+ if s == nil || s .nilSlice {
443+ return len ("null" )
444+ }
445+ if len (s .itemBytes ) == 0 {
446+ return len ("[]" )
447+ }
448+ total := len ("[]" ) + len (s .itemBytes ) - 1
449+ for _ , bytes := range s .itemBytes {
450+ total += bytes
451+ }
452+ return total
453+ }
454+
455+ func (s * jsonArrayTokenSizer [T ]) drop (index int ) {
456+ if s == nil || index < 0 || index >= len (s .itemBytes ) {
457+ return
458+ }
459+ s .nilSlice = false
460+ s .itemBytes = append (s .itemBytes [:index ], s .itemBytes [index + 1 :]... )
461+ }
462+
463+ func (s * jsonArrayTokenSizer [T ]) update (index int , item T ) {
464+ if s == nil || index < 0 || index >= len (s .itemBytes ) {
465+ return
466+ }
467+ s .nilSlice = false
468+ s .itemBytes [index ] = jsonEncodedBytes (item )
469+ }
470+
471+ func jsonEncodedBytes (value any ) int {
472+ data , err := json .Marshal (value )
473+ if err != nil {
474+ return 0
475+ }
476+ return len (data )
477+ }
478+
479+ func approxTokensForBytes (bytes int ) int {
480+ if bytes <= 0 {
481+ return 0
482+ }
483+ return (bytes + 3 ) / 4
484+ }
485+
407486func approxTokensForString (value string ) int {
408487 if value == "" {
409488 return 0
410489 }
411- return (len (value ) + 3 ) / 4
490+ return approxTokensForBytes (len (value ))
412491}
413492
414493func tokensToApproxChars (tokens int ) int {
0 commit comments