2222_COMPRESSION_DROP_MIN_TOKENS = 5_000
2323_COMPRESSION_DROP_RATIO = 0.15
2424
25+ # Context window sizes by model family (tokens)
26+ # Ordered longest-prefix-first for correct matching
27+ _CONTEXT_WINDOW_SIZES : list [tuple [str , int ]] = [
28+ ("gpt-4o-mini" , 128_000 ),
29+ ("gpt-4-turbo" , 128_000 ),
30+ ("gpt-4o" , 128_000 ),
31+ ("claude" , 200_000 ),
32+ ]
33+ _DEFAULT_CONTEXT_WINDOW = 128_000
34+ _OVERFLOW_RISK_HORIZON_MULTIPLIER = 2 # warn if overflow within 2x current turn count
35+
2536
2637def analyze_session_insights (session : Session | None ) -> dict [str , Any ]:
2738 """Summarize session-level carryover, budget, and artifact lifecycle signals."""
@@ -41,6 +52,7 @@ def analyze_session_insights(session: Session | None) -> dict[str, Any]:
4152 artifacts = _artifact_lifecycles (blocks_by_request )
4253 artifact_duplications = _artifact_duplications (session )
4354 propagation = _propagation_graph (blocks_by_request )
55+ forecast = budget_forecast (session )
4456 hints = _build_hints (carryover , budget_events , artifacts , artifact_duplications )
4557
4658 return {
@@ -49,6 +61,7 @@ def analyze_session_insights(session: Session | None) -> dict[str, Any]:
4961 "artifact_lifecycles" : artifacts ,
5062 "artifact_duplications" : artifact_duplications ,
5163 "propagation" : propagation ,
64+ "budget_forecast" : forecast ,
5265 "hints" : hints ,
5366 }
5467
@@ -525,3 +538,58 @@ def _find_first_key(value: Any, keys: tuple[str, ...]) -> Any | None:
525538 if found is not None :
526539 return found
527540 return None
541+
542+
543+ # ---------------------------------------------------------------------------
544+ # Budget Forecast
545+ # ---------------------------------------------------------------------------
546+
547+
548+ def _resolve_context_window (model : str ) -> tuple [str , int ]:
549+ """Match a model string to a known context window size.
550+
551+ Returns (matched_model_family, context_window_tokens).
552+ """
553+ lowered = model .lower ()
554+ for prefix , size in _CONTEXT_WINDOW_SIZES :
555+ if prefix in lowered :
556+ return prefix , size
557+ return "default" , _DEFAULT_CONTEXT_WINDOW
558+
559+
560+ def budget_forecast (session : Session ) -> dict [str , Any ] | None :
561+ """Predict when a session will hit the context window limit.
562+
563+ Returns None for sessions with fewer than 2 requests (not enough data).
564+ """
565+ if session is None or len (session .requests ) < 2 :
566+ return None
567+
568+ token_counts = [req .total_input_tokens for req in session .requests ]
569+ num_turns = len (token_counts )
570+
571+ # Determine model from the last request (most representative)
572+ model_raw = session .requests [- 1 ].model or "unknown"
573+ matched_model , context_window = _resolve_context_window (model_raw )
574+
575+ # Calculate turn-over-turn deltas for growth rate
576+ deltas = [token_counts [i ] - token_counts [i - 1 ] for i in range (1 , num_turns )]
577+ growth_rate = sum (deltas ) / len (deltas ) if deltas else 0.0
578+
579+ current_tokens = token_counts [- 1 ]
580+ current_utilization = current_tokens / context_window if context_window else 0.0
581+
582+ # Predict overflow turn (linear extrapolation from current position)
583+ estimated_overflow_turn : int | None = None
584+ if growth_rate > 0 :
585+ remaining = context_window - current_tokens
586+ turns_until_overflow = remaining / growth_rate
587+ estimated_overflow_turn = num_turns + int (turns_until_overflow )
588+
589+ return {
590+ "growth_rate_per_turn" : round (growth_rate , 1 ),
591+ "current_utilization" : round (current_utilization , 4 ),
592+ "estimated_overflow_turn" : estimated_overflow_turn ,
593+ "context_window_tokens" : context_window ,
594+ "model" : matched_model ,
595+ }
0 commit comments