@@ -24,12 +24,57 @@ impl LlmDreamBackend {
2424 }
2525}
2626
27+ /// Max transcript characters fed to the model in one call. The hard wall is
28+ /// the ~200k-token context limit (a real session hit ~220k tokens and `claude
29+ /// -p` returned HTTP 400). We stay well under it and split oversized
30+ /// transcripts across several calls, merging the events (run_dream dedups).
31+ const TRANSCRIPT_CHAR_BUDGET : usize = 360_000 ;
32+
2733impl DreamBackend for LlmDreamBackend {
2834 fn backfill ( & self , input : & BackfillInput ) -> anyhow:: Result < Vec < BackfillEvent > > {
29- let prompt = crate :: dream:: prompt:: build_prompt ( input) ;
30- let text = self . llm . complete ( & prompt, 1024 ) ?;
31- parse_backfill_json ( & text)
35+ let mut out = Vec :: new ( ) ;
36+ for chunk in chunk_transcript ( & input. transcript , TRANSCRIPT_CHAR_BUDGET ) {
37+ let chunk_input = BackfillInput {
38+ tasks : input. tasks . clone ( ) ,
39+ transcript : chunk,
40+ } ;
41+ let prompt = crate :: dream:: prompt:: build_prompt ( & chunk_input) ;
42+ let text = self . llm . complete ( & prompt, 1024 ) ?;
43+ out. extend ( parse_backfill_json ( & text) ?) ;
44+ }
45+ Ok ( out)
46+ }
47+ }
48+
49+ /// Split a transcript into chunks of at most `budget` bytes, breaking on line
50+ /// boundaries where possible (a lone oversized line is hard-split on char
51+ /// boundaries). Always returns at least one chunk so an empty transcript still
52+ /// yields a single call.
53+ fn chunk_transcript ( transcript : & str , budget : usize ) -> Vec < String > {
54+ if transcript. len ( ) <= budget {
55+ return vec ! [ transcript. to_string( ) ] ;
56+ }
57+ let mut chunks = Vec :: new ( ) ;
58+ let mut cur = String :: new ( ) ;
59+ for line in transcript. split_inclusive ( '\n' ) {
60+ if !cur. is_empty ( ) && cur. len ( ) + line. len ( ) > budget {
61+ chunks. push ( std:: mem:: take ( & mut cur) ) ;
62+ }
63+ if line. len ( ) > budget {
64+ for ch in line. chars ( ) {
65+ if !cur. is_empty ( ) && cur. len ( ) + ch. len_utf8 ( ) > budget {
66+ chunks. push ( std:: mem:: take ( & mut cur) ) ;
67+ }
68+ cur. push ( ch) ;
69+ }
70+ } else {
71+ cur. push_str ( line) ;
72+ }
73+ }
74+ if !cur. is_empty ( ) {
75+ chunks. push ( cur) ;
3276 }
77+ chunks
3378}
3479
3580/// Parse the model's reply (a JSON array of `BackfillEvent`, possibly wrapped in
@@ -66,6 +111,57 @@ mod tests {
66111 assert ! ( parse_backfill_json( "[]" ) . unwrap( ) . is_empty( ) ) ;
67112 }
68113
114+ #[ test]
115+ fn small_transcript_is_one_chunk ( ) {
116+ let c = chunk_transcript ( "a\n b\n c\n " , 100 ) ;
117+ assert_eq ! ( c. len( ) , 1 ) ;
118+ assert_eq ! ( c[ 0 ] , "a\n b\n c\n " ) ;
119+ }
120+
121+ #[ test]
122+ fn big_transcript_splits_on_lines_and_preserves_content ( ) {
123+ // 10 lines of 20 chars; budget 50 → multiple chunks, no loss.
124+ let transcript: String = ( 0 ..10 ) . map ( |i| format ! ( "line{i:015}\n " ) ) . collect ( ) ;
125+ let chunks = chunk_transcript ( & transcript, 50 ) ;
126+ assert ! ( chunks. len( ) > 1 , "must split" ) ;
127+ assert ! ( chunks. iter( ) . all( |c| c. len( ) <= 50 ) ) ;
128+ assert_eq ! ( chunks. concat( ) , transcript, "no content lost" ) ;
129+ }
130+
131+ #[ test]
132+ fn oversized_single_line_is_hard_split ( ) {
133+ let line = "x" . repeat ( 250 ) ;
134+ let chunks = chunk_transcript ( & line, 100 ) ;
135+ assert ! ( chunks. len( ) >= 3 ) ;
136+ assert ! ( chunks. iter( ) . all( |c| c. len( ) <= 100 ) ) ;
137+ assert_eq ! ( chunks. concat( ) , line) ;
138+ }
139+
140+ #[ test]
141+ fn backfill_chunks_large_transcript_into_multiple_calls ( ) {
142+ use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
143+ struct CountingLlm ( AtomicUsize ) ;
144+ impl LlmBackend for CountingLlm {
145+ fn complete ( & self , _prompt : & str , _max : u32 ) -> anyhow:: Result < String > {
146+ self . 0 . fetch_add ( 1 , Ordering :: SeqCst ) ;
147+ Ok ( "[]" . to_string ( ) )
148+ }
149+ fn name ( & self ) -> & ' static str {
150+ "counting"
151+ }
152+ }
153+ let llm = Box :: new ( CountingLlm ( AtomicUsize :: new ( 0 ) ) ) ;
154+ // Build a transcript larger than the budget so it must split.
155+ let transcript = "y\n " . repeat ( TRANSCRIPT_CHAR_BUDGET ) ;
156+ let b = LlmDreamBackend :: new ( llm) ;
157+ let input = BackfillInput {
158+ tasks : vec ! [ ] ,
159+ transcript,
160+ } ;
161+ let evs = b. backfill ( & input) . unwrap ( ) ;
162+ assert ! ( evs. is_empty( ) ) ;
163+ }
164+
69165 #[ test]
70166 fn llm_dream_backend_runs_and_parses ( ) {
71167 struct FakeLlm ;
0 commit comments