12
12
//! of the conversation so far and then start a new session where the first
13
13
//! message contains the summary
14
14
15
- use std:: sync:: Arc ;
15
+ use std:: { collections :: HashMap , sync:: Arc } ;
16
16
17
17
use anyhow:: { Context , Result } ;
18
18
use but_broadcaster:: Broadcaster ;
19
19
use but_workspace:: StackId ;
20
20
use gitbutler_command_context:: CommandContext ;
21
21
use gix:: bstr:: ByteSlice ;
22
+ use serde:: Deserialize ;
22
23
use tokio:: {
23
24
process:: Command ,
24
25
sync:: { Mutex , mpsc:: unbounded_channel} ,
@@ -32,6 +33,47 @@ use crate::{
32
33
send_claude_message,
33
34
} ;
34
35
36
+ #[ derive( Deserialize , Debug , Clone ) ]
37
+ #[ serde( rename_all = "camelCase" ) ]
38
+ struct ModelUsage {
39
+ input_tokens : u32 ,
40
+ output_tokens : u32 ,
41
+ cache_read_input_tokens : Option < u32 > ,
42
+ }
43
+
44
+ #[ derive( Debug ) ]
45
+ struct Model < ' a > {
46
+ name : & ' a str ,
47
+ subtype : Option < & ' a str > ,
48
+ context : u32 ,
49
+ }
50
+
51
+ const COMPACTION_BUFFER : u32 = 15_000 ;
52
+
53
+ const MODELS : & [ Model < ' static > ] = & [
54
+ Model {
55
+ name : "opus" ,
56
+ subtype : None ,
57
+ context : 200_000 ,
58
+ } ,
59
+ // Ordering the 1m model before the 200k model so it matches first.
60
+ Model {
61
+ name : "sonnet" ,
62
+ subtype : Some ( "[1m]" ) ,
63
+ context : 1_000_000 ,
64
+ } ,
65
+ Model {
66
+ name : "sonnet" ,
67
+ subtype : None ,
68
+ context : 200_000 ,
69
+ } ,
70
+ Model {
71
+ name : "haiku" ,
72
+ subtype : None ,
73
+ context : 200_000 ,
74
+ } ,
75
+ ] ;
76
+
35
77
impl Claudes {
36
78
pub ( crate ) async fn compact (
37
79
& self ,
@@ -122,6 +164,66 @@ impl Claudes {
122
164
123
165
Ok ( ( ) )
124
166
}
167
+
168
+ pub ( crate ) async fn maybe_compact_context (
169
+ & self ,
170
+ ctx : Arc < Mutex < CommandContext > > ,
171
+ broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
172
+ stack_id : StackId ,
173
+ ) -> Result < ( ) > {
174
+ let rule = {
175
+ let mut ctx = ctx. lock ( ) . await ;
176
+ list_claude_assignment_rules ( & mut ctx) ?
177
+ . into_iter ( )
178
+ . find ( |rule| rule. stack_id == stack_id)
179
+ } ;
180
+ let Some ( rule) = rule else {
181
+ return Ok ( ( ) ) ;
182
+ } ;
183
+
184
+ let messages = {
185
+ let mut ctx = ctx. lock ( ) . await ;
186
+ db:: list_messages_by_session ( & mut ctx, rule. session_id ) ?
187
+ } ;
188
+
189
+ // Find the last result message
190
+ let Some ( output) = messages. into_iter ( ) . rev ( ) . find_map ( |m| match m. content {
191
+ ClaudeMessageContent :: ClaudeOutput ( o) => {
192
+ if o[ "type" ] . as_str ( ) == Some ( "result" ) {
193
+ Some ( o)
194
+ } else {
195
+ None
196
+ }
197
+ }
198
+ _ => None ,
199
+ } ) else {
200
+ return Ok ( ( ) ) ;
201
+ } ;
202
+
203
+ let usage: HashMap < String , ModelUsage > =
204
+ serde_json:: from_value ( output[ "modelUsage" ] . clone ( ) ) ?;
205
+
206
+ for ( name, usage) in usage {
207
+ if let Some ( model) = find_model ( name) {
208
+ let total = usage. cache_read_input_tokens . unwrap_or ( 0 )
209
+ + usage. input_tokens
210
+ + usage. output_tokens ;
211
+ if total > ( model. context - COMPACTION_BUFFER ) {
212
+ self . compact ( ctx. clone ( ) , broadcaster. clone ( ) , stack_id)
213
+ . await ;
214
+ break ;
215
+ }
216
+ } ;
217
+ }
218
+
219
+ Ok ( ( ) )
220
+ }
221
+ }
222
+
223
+ fn find_model ( name : String ) -> Option < & ' static Model < ' static > > {
224
+ MODELS
225
+ . iter ( )
226
+ . find ( |& m| name. contains ( m. name ) && m. subtype . map ( |s| name. contains ( s) ) . unwrap_or ( true ) )
125
227
}
126
228
127
229
pub async fn generate_summary (
0 commit comments