1+ package engine
2+
3+ import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/GrayCodeAI/hawk/internal/engine/ctxmgr"
8+ modelPkg "github.com/GrayCodeAI/hawk/internal/provider/routing"
9+ )
10+
11+ const (
12+ // DefaultContextWindow is used when the model catalog has no context size.
13+ DefaultContextWindow = 128_000
14+ // DefaultAutoCompactThresholdPct matches Grok CLI default (85% of window).
15+ DefaultAutoCompactThresholdPct = 85
16+ )
17+
18+ // ResolveModelContextWindow returns the effective context window for a model.
19+ func ResolveModelContextWindow (model string , override int ) int {
20+ if override > 0 {
21+ return override
22+ }
23+ if info , ok := modelPkg .Find (model ); ok && info .ContextSize > 0 {
24+ return info .ContextSize
25+ }
26+ return DefaultContextWindow
27+ }
28+
29+ // ContextWindowSize returns this session's context window (catalog or default).
30+ func (s * Session ) ContextWindowSize () int {
31+ if s == nil {
32+ return DefaultContextWindow
33+ }
34+ return ResolveModelContextWindow (s .model , s .ContextWindowCached )
35+ }
36+
37+ // EnsureAutoCompactor initializes the compaction orchestrator from session settings.
38+ func (s * Session ) EnsureAutoCompactor () {
39+ if s == nil {
40+ return
41+ }
42+ if s .AutoCompactor != nil {
43+ s .AutoCompactor .Configure (s .compactConfig ())
44+ return
45+ }
46+ s .AutoCompactor = NewAutoCompactor (s .compactConfig ())
47+ }
48+
49+ func (s * Session ) compactThresholdPct () int {
50+ pct := s .AutoCompactThresholdPct
51+ if pct <= 0 {
52+ pct = DefaultAutoCompactThresholdPct
53+ }
54+ if pct < 50 {
55+ pct = 50
56+ }
57+ if pct > 95 {
58+ pct = 95
59+ }
60+ return pct
61+ }
62+
63+ func (s * Session ) compactConfig () CompactConfig {
64+ window := s .ContextWindowSize ()
65+ pct := s .compactThresholdPct ()
66+ target := window * pct / 100
67+ cfg := DefaultCompactConfig ()
68+ cfg .AutoEnabled = true
69+ cfg .ContextWindowSize = window
70+ cfg .MaxOutputTokens = 0
71+ cfg .AutoCompactBuffer = window - target
72+ if cfg .AutoCompactBuffer < 0 {
73+ cfg .AutoCompactBuffer = 0
74+ }
75+ return cfg
76+ }
77+
78+ // refreshContextWindowCache updates cached window from the catalog when the model changes.
79+ func (s * Session ) refreshContextWindowCache () {
80+ if s == nil {
81+ return
82+ }
83+ s .ContextWindowCached = 0
84+ if info , ok := modelPkg .Find (s .model ); ok && info .ContextSize > 0 {
85+ s .ContextWindowCached = info .ContextSize
86+ }
87+ s .EnsureAutoCompactor ()
88+ }
89+
90+ // ManageContextBeforeTurn collapses noise, then compacts via the strategy registry when needed.
91+ // Returns the compaction strategy name (if any) and whether messages were reduced.
92+ func (s * Session ) ManageContextBeforeTurn (ctx context.Context ) (strategy string , compacted bool ) {
93+ if s == nil {
94+ return "" , false
95+ }
96+ s .messages = ctxmgr .CollapseRepeatedMessages (s .messages )
97+
98+ s .EnsureAutoCompactor ()
99+ if strat , ok := s .AutoCompactor .AutoCompactIfNeeded (ctx , s ); ok {
100+ return strat , true
101+ }
102+
103+ if len (s .messages ) > maxContextMessages {
104+ s .smartCompact ()
105+ return "smart_message_cap" , true
106+ }
107+
108+ convTokens := EstimateTokens (s .messages )
109+ window := s .ContextWindowSize ()
110+ budget := ctxmgr .NewContextBudget (window )
111+ if budget .ShouldCompact (convTokens ) {
112+ s .smartCompact ()
113+ return "smart_budget" , true
114+ }
115+
116+ return "" , false
117+ }
118+
119+ // CompactConversation runs compaction immediately (for /compact). Uses the full strategy chain.
120+ func (s * Session ) CompactConversation (ctx context.Context ) (strategy string , tokensBefore , tokensAfter int , err error ) {
121+ if s == nil {
122+ return "" , 0 , 0 , fmt .Errorf ("no session" )
123+ }
124+ s .messages = ctxmgr .CollapseRepeatedMessages (s .messages )
125+ s .EnsureAutoCompactor ()
126+ tokensBefore = EstimateTokens (s .messages )
127+ strategy , err = s .AutoCompactor .RunCompaction (ctx , s )
128+ if err != nil {
129+ s .smartCompact ()
130+ strategy = "smart_fallback"
131+ }
132+ tokensAfter = EstimateTokens (s .messages )
133+ return strategy , tokensBefore , tokensAfter , nil
134+ }
135+
136+ // ShouldCompactByBudget reports whether conversation tokens exceed the configured % of window.
137+ func (s * Session ) ShouldCompactByBudget () bool {
138+ window := s .ContextWindowSize ()
139+ conv := EstimateTokens (s .messages )
140+ return conv >= window * s .compactThresholdPct ()/ 100
141+ }
0 commit comments