package vip.mate.agent.context; /** * Configuration for per-reasoning-loop message budgeting. * *

Used by {@link LoopMessageBudgeter} to decide when and how to trim the * working message list that a ReAct iteration hands to the LLM. Distinct from * the multi-turn history compression configured by * {@link vip.mate.config.ConversationWindowProperties}: this one applies inside * a single user turn while the ReAct loop accumulates reasoning steps and * tool-call/tool-response pairs. * *

Field semantics: *

*/ public record LoopBudgetConfig( int triggerTokens, int keepTailTokens, int minTailMessages, double tailSoftCeilingRatio, int reservedPrefixTokens, int targetMaxMessages) { /** Smallest useful trigger threshold; below this budgeting is effectively disabled. */ public static final int MIN_TRIGGER_TOKENS = 1_000; /** Smallest representable explicit remaining-history budget. */ public static final int MIN_TAIL_TOKENS = 1; /** Default policy floor; explicit available-window clipping may go lower. */ public static final int DEFAULT_MIN_TAIL_TOKENS = 2_000; /** Floor on minTailMessages — fewer than 3 collapses recent context too aggressively. */ public static final int MIN_TAIL_MESSAGES_FLOOR = 3; /** Floor on the soft ceiling ratio — anything below 1.0 is degenerate. */ public static final double MIN_TAIL_SOFT_CEILING_RATIO = 1.0; /** Smallest sensible target cap; below this even a normal ReAct loop trips it. */ public static final int MIN_TARGET_MAX = 20; public LoopBudgetConfig { if (triggerTokens < MIN_TRIGGER_TOKENS) { throw new IllegalArgumentException( "triggerTokens must be >= " + MIN_TRIGGER_TOKENS + ", got " + triggerTokens); } if (keepTailTokens < MIN_TAIL_TOKENS) { throw new IllegalArgumentException( "keepTailTokens must be >= " + MIN_TAIL_TOKENS + ", got " + keepTailTokens); } if (minTailMessages < MIN_TAIL_MESSAGES_FLOOR) { throw new IllegalArgumentException( "minTailMessages must be >= " + MIN_TAIL_MESSAGES_FLOOR + ", got " + minTailMessages); } if (tailSoftCeilingRatio < MIN_TAIL_SOFT_CEILING_RATIO) { throw new IllegalArgumentException( "tailSoftCeilingRatio must be >= " + MIN_TAIL_SOFT_CEILING_RATIO + ", got " + tailSoftCeilingRatio); } if (reservedPrefixTokens < 0) { throw new IllegalArgumentException( "reservedPrefixTokens must be >= 0, got " + reservedPrefixTokens); } if (targetMaxMessages < MIN_TARGET_MAX) { throw new IllegalArgumentException( "targetMaxMessages must be >= " + MIN_TARGET_MAX + ", got " + targetMaxMessages); } if (keepTailTokens >= triggerTokens) { throw new IllegalArgumentException( "keepTailTokens (" + keepTailTokens + ") must be < triggerTokens (" + triggerTokens + ") — otherwise budgeting would never reduce anything"); } } /** Tail budget after applying the soft ceiling. */ public int tailSoftCeilingTokens() { return (int) (keepTailTokens * tailSoftCeilingRatio); } /** * Derive a sensible config from a model's context window. The ratios were * chosen so the budgeter triggers well before the model's actual limit and * leaves enough headroom for the LLM's own response. * * */ public static LoopBudgetConfig forContext(int contextWindowTokens) { if (contextWindowTokens <= 0) { contextWindowTokens = 32_000; } // Tiny reported windows still need a valid policy configuration so callers // can reject an impossible protected prefix rather than fail in this constructor. int trigger = Math.max(DEFAULT_MIN_TAIL_TOKENS + MIN_TRIGGER_TOKENS, (int) (contextWindowTokens * 0.50)); int tail = Math.max(DEFAULT_MIN_TAIL_TOKENS, (int) (contextWindowTokens * 0.30)); if (tail >= trigger) { tail = Math.max(DEFAULT_MIN_TAIL_TOKENS, trigger - MIN_TRIGGER_TOKENS); } return new LoopBudgetConfig(trigger, tail, 4, 1.5, 0, 200); } /** * Clip the recent-history policy to the actual remaining window. The * default 2000-token policy floor is not a permission to overflow it. * An indivisible latest tool pair may still exceed this estimate; callers * must reject that assembled prompt before contacting the provider. */ public LoopBudgetConfig withAvailableHistoryTokens(int availableTokens) { int tail = Math.min(keepTailTokens, Math.max(MIN_TAIL_TOKENS, availableTokens)); double ratio = Math.min(tailSoftCeilingRatio, Math.max(1.0, (double)availableTokens / tail)); return new LoopBudgetConfig(triggerTokens, tail, minTailMessages, ratio, reservedPrefixTokens, targetMaxMessages); } /** Return a copy with {@code reservedPrefixTokens} replaced. */ public LoopBudgetConfig withReservedPrefixTokens(int reservedPrefixTokens) { return new LoopBudgetConfig(triggerTokens, keepTailTokens, minTailMessages, tailSoftCeilingRatio, reservedPrefixTokens, targetMaxMessages); } }