lean_ctx/tools/
ctx_multi_read.rs1use crate::core::cache::SessionCache;
2use crate::core::heatmap;
3use crate::core::tokens::count_tokens;
4use crate::tools::CrpMode;
5use crate::tools::ctx_read;
6
7pub fn handle(cache: &mut SessionCache, paths: &[String], mode: &str, crp_mode: CrpMode) -> String {
8 handle_with_task(cache, paths, mode, crp_mode, None)
9}
10
11pub fn handle_with_task(
12 cache: &mut SessionCache,
13 paths: &[String],
14 mode: &str,
15 crp_mode: CrpMode,
16 task: Option<&str>,
17) -> String {
18 handle_with_task_fresh(cache, paths, mode, false, crp_mode, task)
19}
20
21const DEFAULT_MAX_MULTI_READ_BYTES: usize = 512 * 1024;
22
23fn max_multi_read_bytes() -> usize {
24 std::env::var("LCTX_MAX_MULTI_READ_BYTES")
25 .ok()
26 .and_then(|v| v.parse().ok())
27 .unwrap_or(DEFAULT_MAX_MULTI_READ_BYTES)
28}
29
30pub fn handle_with_task_fresh(
31 cache: &mut SessionCache,
32 paths: &[String],
33 mode: &str,
34 fresh: bool,
35 crp_mode: CrpMode,
36 task: Option<&str>,
37) -> String {
38 let n = paths.len();
39 if n == 0 {
40 return "Read 0 files | 0 tokens saved".to_string();
41 }
42
43 let max_bytes = max_multi_read_bytes();
44 let mut sections: Vec<String> = Vec::with_capacity(n);
45 let mut total_saved: usize = 0;
46 let mut total_original: usize = 0;
47 let mut accumulated_bytes: usize = 0;
48 let mut files_read = 0usize;
49 let mut truncated = false;
50
51 for path in paths {
52 let effective_mode = if ctx_read::is_instruction_file(path) {
53 "full"
54 } else {
55 mode
56 };
57 let chunk = if fresh {
58 ctx_read::handle_fresh_with_task(cache, path, effective_mode, crp_mode, task)
59 } else {
60 ctx_read::handle_with_task(cache, path, effective_mode, crp_mode, task)
61 };
62 let original = cache.get(path).map_or(0, |e| e.original_tokens);
63 let sent = count_tokens(&chunk);
64 heatmap::record_file_access(path, original, original.saturating_sub(sent));
65 {
70 use crate::core::savings_ledger as ledger;
71 let (lbase, lsaved) =
72 if ledger::ledger_family() == crate::core::tokens::TokenizerFamily::O200kBase {
73 (original, original.saturating_sub(sent))
74 } else if let Some(raw) = cache
75 .get(path)
76 .and_then(crate::core::cache::CacheEntry::content)
77 {
78 let lo = ledger::count_for_ledger(&raw);
79 (lo, lo.saturating_sub(ledger::count_for_ledger(&chunk)))
80 } else {
81 (original, original.saturating_sub(sent))
82 };
83 ledger::record_read_event(lbase, lsaved);
84 }
85 total_original = total_original.saturating_add(original);
86 total_saved = total_saved.saturating_add(original.saturating_sub(sent));
87
88 let chunk_bytes = chunk.len();
89 if accumulated_bytes > 0 && accumulated_bytes + chunk_bytes > max_bytes {
90 truncated = true;
91 break;
92 }
93 accumulated_bytes += chunk_bytes;
94 sections.push(chunk);
95 files_read += 1;
96 }
97
98 let body = sections.join("\n---\n");
99 let summary = if truncated {
100 let skipped = n - files_read;
101 format!(
102 "Read {files_read}/{n} files | {total_saved} tokens saved\n\
103 ⚠ Output capped at {max_bytes} bytes (LCTX_MAX_MULTI_READ_BYTES). \
104 {skipped} file(s) skipped. Use individual ctx_read calls for remaining files."
105 )
106 } else {
107 format!("Read {n} files | {total_saved} tokens saved")
108 };
109 format!("{body}\n---\n{summary}")
110}