| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | |
| 6 | "reasonix/internal/provider" |
| 7 | ) |
| 8 | |
| 9 | // summaryPlanMarginRatio is the planning headroom left under the window for |
| 10 | // estimator error. The fixed protocol reserve alone is under 1% of a 1M |
| 11 | // window, thinner than the tokenizer drift real sessions show (#9818). |
| 12 | const summaryPlanMarginRatio = 0.05 |
| 13 | |
| 14 | func summaryPlanReserve(window int) int { |
| 15 | return max(protocolReserveTokens, int(float64(window)*summaryPlanMarginRatio)) |
| 16 | } |
| 17 | |
| 18 | // maximumSafeSummaryPrefixEnd returns the largest balanced contiguous prefix |
| 19 | // whose exact summary request leaves the collector's minimum output budget. |
| 20 | // The remaining middle and tail stay verbatim in the projection. |
| 21 | func (a *Agent) maximumSafeSummaryPrefixEnd(msgs []provider.Message, head, end int, instructions string) int { |
| 22 | if head < 0 || end <= head || end > len(msgs) { |
| 23 | return end |
| 24 | } |
| 25 | maxPromptTokens, enforce := a.safeSummaryPromptTokenLimit() |
| 26 | if !enforce { |
| 27 | return end |
| 28 | } |
| 29 | if maxPromptTokens <= 0 { |
| 30 | return head |
| 31 | } |
| 32 | fits := func(candidate int) bool { |
| 33 | fold, _ := withoutPinnedContextRevisions(msgs[head:candidate]) |
| 34 | request := a.summaryRequest(fold, instructions) |
| 35 | return a.estimatedRequestTokens(request) <= maxPromptTokens |
| 36 | } |
| 37 | if fits(end) { |
| 38 | return end |
| 39 | } |
| 40 | |
| 41 | low, high, best := head+1, end-1, head |
| 42 | for low <= high { |
| 43 | mid := low + (high-low)/2 |
| 44 | if fits(mid) { |
| 45 | best = mid |
| 46 | low = mid + 1 |
| 47 | } else { |
| 48 | high = mid - 1 |
| 49 | } |
| 50 | } |
| 51 | // A tail beginning with a tool result would split it from the assistant |
| 52 | // tool-call message. Move the fold boundary back across the whole result |
| 53 | // group; the assistant call and all of its results then remain together. |
| 54 | for best > head && best < len(msgs) && msgs[best].Role == provider.RoleTool { |
| 55 | best-- |
| 56 | } |
| 57 | return best |
| 58 | } |
| 59 | |
| 60 | // safeSummaryPromptTokenLimit is shared by prefix planning and the final |
| 61 | // post-extension guard. Unknown gateways conservatively honor the configured |
| 62 | // or learned window; explicitly independent providers retain the full fold. |
| 63 | func (a *Agent) safeSummaryPromptTokenLimit() (int, bool) { |
| 64 | window := a.effectiveContextWindow() |
| 65 | if window <= 0 || contextBudgetPolicyOf(a.svc.prov).WindowMode == provider.ContextWindowIndependent { |
| 66 | return 0, false |
| 67 | } |
| 68 | return window - a.summaryOutputBudget() - summaryPlanReserve(window), true |
| 69 | } |
| 70 | |
| 71 | // validateSafeSummaryRequest guards the final fold in the request form that |
| 72 | // will actually be sent. |
| 73 | func (a *Agent) validateSafeSummaryRequest(fold []provider.Message, instructions string, slim bool) error { |
| 74 | maxPromptTokens, enforce := a.safeSummaryPromptTokenLimit() |
| 75 | if !enforce { |
| 76 | return nil |
| 77 | } |
| 78 | request := a.summaryRequest(fold, instructions) |
| 79 | if slim { |
| 80 | request = a.slimSummaryRequest(fold, instructions) |
| 81 | } |
| 82 | requestTokens := a.estimatedRequestTokens(request) |
| 83 | if maxPromptTokens <= 0 || requestTokens > maxPromptTokens { |
| 84 | return fmt.Errorf("%w: prepared summary request (%d tokens) exceeds safe prompt budget (%d)", |
| 85 | errCheckpointRejected, requestTokens, maxPromptTokens) |
| 86 | } |
| 87 | return nil |
| 88 | } |
| 89 |