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 sensible tail budget; below this even one observation may not fit. */ public static final int 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; } int trigger = Math.max(MIN_TRIGGER_TOKENS, (int) (contextWindowTokens * 0.50)); int tail = Math.max(MIN_TAIL_TOKENS, (int) (contextWindowTokens * 0.30)); if (tail >= trigger) { tail = Math.max(MIN_TAIL_TOKENS, trigger - MIN_TRIGGER_TOKENS); } return new LoopBudgetConfig(trigger, tail, 4, 1.5, 0, 200); } /** Return a copy with {@code reservedPrefixTokens} replaced. */ public LoopBudgetConfig withReservedPrefixTokens(int reservedPrefixTokens) { return new LoopBudgetConfig(triggerTokens, keepTailTokens, minTailMessages, tailSoftCeilingRatio, reservedPrefixTokens, targetMaxMessages); } }