| 1 | package agent |
| 2 | |
| 3 | import "reasonix/internal/provider" |
| 4 | |
| 5 | // foldRequest is what one fold attempt asks of the summarizer. force shrinks |
| 6 | // the verbatim tail, mustFree caps the summary input to the safe prefix, and |
| 7 | // allowChunked permits the multi-request fragment path after a size failure. |
| 8 | type foldRequest struct { |
| 9 | force, mustFree, allowChunked bool |
| 10 | // slim renders the fold as one bounded transcript instead of the |
| 11 | // cache-aligned replay. It is a rung on the overflow ladder, never a default. |
| 12 | slim bool |
| 13 | } |
| 14 | |
| 15 | // summaryInputModeFor labels the summarizer input for telemetry and the |
| 16 | // chunked-fallback gate. The slim rung overrides the replay-shape labels. |
| 17 | func summaryInputModeFor(req foldRequest, pinned, rewritten bool) string { |
| 18 | switch { |
| 19 | case req.slim: |
| 20 | return SummaryInputSlim |
| 21 | case pinned: |
| 22 | return SummaryInputNonPrefix |
| 23 | case rewritten: |
| 24 | return SummaryInputExtensionRewritten |
| 25 | default: |
| 26 | return SummaryInputCachePrefix |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // Overflow ladder for one maintenance transaction: replay-form summaries first |
| 31 | // (each re-planned on the calibration a provider overflow just corrected), |
| 32 | // then one transcript-form summary, then the fragment path when allowed. |
| 33 | const maxSummaryReplans = 2 |
| 34 | |
| 35 | // summaryLadder paces the fold attempts of one maintenance transaction and |
| 36 | // absorbs provider overflows by moving to the next rung instead of failing. |
| 37 | type summaryLadder struct { |
| 38 | remaining int // successful summaries still allowed by the trigger's policy |
| 39 | replans int // overflow re-plans consumed |
| 40 | slim bool |
| 41 | } |
| 42 | |
| 43 | func newSummaryLadder(maxSummaries int) *summaryLadder { |
| 44 | return &summaryLadder{remaining: maxSummaries} |
| 45 | } |
| 46 | |
| 47 | // next reports whether another fold attempt may start and consumes one slot. |
| 48 | func (l *summaryLadder) next() bool { |
| 49 | if l.remaining <= 0 { |
| 50 | return false |
| 51 | } |
| 52 | l.remaining-- |
| 53 | return true |
| 54 | } |
| 55 | |
| 56 | func (l *summaryLadder) request(force, mustFree, allowChunked bool) foldRequest { |
| 57 | return foldRequest{force: force, mustFree: mustFree, allowChunked: allowChunked, slim: l.slim} |
| 58 | } |
| 59 | |
| 60 | // absorbOverflow moves to the next rung after a provider overflow and returns |
| 61 | // the slot it consumed, so the caller retries without spending a summary. A |
| 62 | // re-plan is only worth a request when the reply carried the prompt count |
| 63 | // that recalibrates it; otherwise the transcript form is the next rung. |
| 64 | func (l *summaryLadder) absorbOverflow(err error) bool { |
| 65 | limit := provider.AsContextLimitError(err) |
| 66 | if limit == nil { |
| 67 | return false |
| 68 | } |
| 69 | switch { |
| 70 | case limit.PromptTokens > 0 && l.replans < maxSummaryReplans && !l.slim: |
| 71 | l.replans++ |
| 72 | case !l.slim: |
| 73 | l.slim = true |
| 74 | default: |
| 75 | return false |
| 76 | } |
| 77 | l.remaining++ |
| 78 | return true |
| 79 | } |
| 80 |