Skip to main content

lean_ctx/tools/
ctx_multi_read.rs

1use crate::core::cache::SessionCache;
2use crate::core::heatmap;
3use crate::core::tokens::count_tokens;
4use crate::tools::ctx_read;
5use crate::tools::CrpMode;
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        total_original = total_original.saturating_add(original);
66        total_saved = total_saved.saturating_add(original.saturating_sub(sent));
67
68        let chunk_bytes = chunk.len();
69        if accumulated_bytes > 0 && accumulated_bytes + chunk_bytes > max_bytes {
70            truncated = true;
71            break;
72        }
73        accumulated_bytes += chunk_bytes;
74        sections.push(chunk);
75        files_read += 1;
76    }
77
78    let body = sections.join("\n---\n");
79    let summary = if truncated {
80        let skipped = n - files_read;
81        format!(
82            "Read {files_read}/{n} files | {total_saved} tokens saved\n\
83             ⚠ Output capped at {max_bytes} bytes (LCTX_MAX_MULTI_READ_BYTES). \
84             {skipped} file(s) skipped. Use individual ctx_read calls for remaining files."
85        )
86    } else {
87        format!("Read {n} files | {total_saved} tokens saved")
88    };
89    format!("{body}\n---\n{summary}")
90}