skilllite_agent/types/env_config.rs
1//! Environment config helpers for long-text summarization, tool result limits, etc.
2//!
3//! Ported from Python `config/env_config.py`.
4
5/// Helper: read an env var as usize with fallback.
6fn env_usize(key: &str, default: usize) -> usize {
7 std::env::var(key)
8 .ok()
9 .and_then(|v| v.parse().ok())
10 .unwrap_or(default)
11}
12
13/// Chunk size for long text summarization (~1.5k tokens). `SKILLLITE_CHUNK_SIZE`.
14pub fn get_chunk_size() -> usize {
15 env_usize("SKILLLITE_CHUNK_SIZE", 6000)
16}
17
18/// Number of head chunks for head+tail summarization. `SKILLLITE_HEAD_CHUNKS`.
19pub fn get_head_chunks() -> usize {
20 env_usize("SKILLLITE_HEAD_CHUNKS", 3)
21}
22
23/// Number of tail chunks for head+tail summarization. `SKILLLITE_TAIL_CHUNKS`.
24pub fn get_tail_chunks() -> usize {
25 env_usize("SKILLLITE_TAIL_CHUNKS", 3)
26}
27
28/// Max output length for summarization (~2k tokens). `SKILLLITE_MAX_OUTPUT_CHARS`.
29pub fn get_max_output_chars() -> usize {
30 env_usize("SKILLLITE_MAX_OUTPUT_CHARS", 8000)
31}
32
33/// Model for Map stage in MapReduce summarization. `SKILLLITE_MAP_MODEL`.
34/// When set, Map (per-chunk summarization) uses this cheaper model; Reduce (merge) uses main model.
35/// E.g. `qwen-plus`, `gemini-1.5-flash`. If unset, both stages use main model.
36pub fn get_map_model(main_model: &str) -> String {
37 skilllite_core::config::loader::env_optional("SKILLLITE_MAP_MODEL", &[])
38 .unwrap_or_else(|| main_model.to_string())
39}
40
41/// Long text selection strategy. `SKILLLITE_LONG_TEXT_STRATEGY`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LongTextStrategy {
44 /// Head + tail only (existing behavior).
45 HeadTailOnly,
46 /// Score all chunks (Position + Discourse + Entity), take top-K.
47 HeadTailExtract,
48 /// Map all chunks (no filtering), Reduce merge. Best with SKILLLITE_MAP_MODEL.
49 MapReduceFull,
50}
51
52fn env_str(key: &str, default: &str) -> String {
53 std::env::var(key).unwrap_or_else(|_| default.to_string())
54}
55
56pub fn get_long_text_strategy() -> LongTextStrategy {
57 let v = env_str("SKILLLITE_LONG_TEXT_STRATEGY", "head_tail_only")
58 .to_lowercase()
59 .trim()
60 .to_string();
61 match v.as_str() {
62 "head_tail_extract" | "extract" => LongTextStrategy::HeadTailExtract,
63 "mapreduce_full" | "mapreduce" | "map_reduce" => LongTextStrategy::MapReduceFull,
64 _ => LongTextStrategy::HeadTailOnly,
65 }
66}
67
68/// Number of chunks to select in extract mode. Uses ratio or head+tail count as floor.
69pub fn get_extract_top_k(total_chunks: usize, head_chunks: usize, tail_chunks: usize) -> usize {
70 let ratio = std::env::var("SKILLLITE_EXTRACT_TOP_K_RATIO")
71 .ok()
72 .and_then(|v| v.parse::<f64>().ok())
73 .unwrap_or(0.5);
74 let by_ratio = (total_chunks as f64 * ratio).ceil() as usize;
75 let floor = head_chunks + tail_chunks;
76 by_ratio.max(floor).min(total_chunks)
77}
78
79/// Threshold above which chunked LLM summarization is used instead of simple
80/// truncation. `SKILLLITE_SUMMARIZE_THRESHOLD`.
81/// Default raised from 15000→30000 to avoid summarizing medium-sized HTML/code
82/// files (e.g. 17KB website) which destroys content needed for downstream tasks.
83pub fn get_summarize_threshold() -> usize {
84 env_usize("SKILLLITE_SUMMARIZE_THRESHOLD", 30000)
85}
86
87/// Max output tokens for LLM completion. `SKILLLITE_MAX_TOKENS`.
88/// Higher values reduce write_output/write_file truncation when generating large content.
89/// Default 8192 to match common API limits (e.g. DeepSeek). Set higher if your API supports it.
90pub fn get_max_tokens() -> usize {
91 env_usize("SKILLLITE_MAX_TOKENS", 8192)
92}
93
94/// Max chars for a single user input message before truncation/summarization.
95/// `SKILLLITE_USER_INPUT_MAX_CHARS`. Default 30000 (~7.5k tokens).
96/// Inputs shorter than this pass through unchanged; longer inputs are
97/// truncated (if ≤ `SKILLLITE_SUMMARIZE_THRESHOLD`) or LLM-summarized.
98pub fn get_user_input_max_chars() -> usize {
99 env_usize("SKILLLITE_USER_INPUT_MAX_CHARS", 30000)
100}
101
102/// Max chars per tool result. `SKILLLITE_TOOL_RESULT_MAX_CHARS`.
103/// Default raised from 8000→12000 to better accommodate HTML/code tool results
104/// without triggering unnecessary truncation.
105pub fn get_tool_result_max_chars() -> usize {
106 env_usize("SKILLLITE_TOOL_RESULT_MAX_CHARS", 12000)
107}
108
109/// Max chars for tool messages during context-overflow recovery.
110/// `SKILLLITE_TOOL_RESULT_RECOVERY_MAX_CHARS`.
111pub fn get_tool_result_recovery_max_chars() -> usize {
112 env_usize("SKILLLITE_TOOL_RESULT_RECOVERY_MAX_CHARS", 3000)
113}
114
115/// Output directory override. `SKILLLITE_OUTPUT_DIR`.
116pub fn get_output_dir() -> Option<String> {
117 skilllite_core::config::PathsConfig::from_env().output_dir
118}
119
120/// Compaction threshold: compact conversation history when message count exceeds this.
121/// `SKILLLITE_COMPACTION_THRESHOLD`. Default 16 (~8 turns).
122pub fn get_compaction_threshold() -> usize {
123 env_usize("SKILLLITE_COMPACTION_THRESHOLD", 16)
124}
125
126/// Whether to run pre-compaction memory flush (OpenClaw-style).
127/// When enabled, before compacting we run a silent agent turn to remind the model
128/// to write durable memories. `SKILLLITE_MEMORY_FLUSH_ENABLED`. Default true.
129pub fn get_memory_flush_enabled() -> bool {
130 let v = skilllite_core::config::loader::env_optional("SKILLLITE_MEMORY_FLUSH_ENABLED", &[]);
131 !matches!(
132 v.as_deref().map(|s| s.to_lowercase()),
133 Some(s) if matches!(s.as_str(), "0" | "false" | "no" | "off")
134 )
135}
136
137/// Memory flush threshold: run memory flush when history approaches compaction.
138/// Lower value = more frequent memory flush. Use same as compaction if not set.
139/// `SKILLLITE_MEMORY_FLUSH_THRESHOLD`. Default 12 (so flush triggers ~4 msgs before compaction at 16).
140pub fn get_memory_flush_threshold() -> usize {
141 env_usize("SKILLLITE_MEMORY_FLUSH_THRESHOLD", 12)
142}
143
144/// Number of recent messages to keep after compaction. `SKILLLITE_COMPACTION_KEEP_RECENT`.
145pub fn get_compaction_keep_recent() -> usize {
146 env_usize("SKILLLITE_COMPACTION_KEEP_RECENT", 10)
147}
148
149/// Whether to use compact planning prompt (rule filtering + fewer examples).
150/// - If SKILLLITE_COMPACT_PLANNING is set: use that (1=compact, 0=full).
151/// - If not set: only latest/best models (claude, gpt-4, gpt-5, gemini-2) use compact; deepseek, qwen, 7b, ollama etc. get full.
152pub fn get_compact_planning(model: Option<&str>) -> bool {
153 if let Some(v) = skilllite_core::config::loader::env_optional(
154 skilllite_core::config::env_keys::misc::SKILLLITE_COMPACT_PLANNING,
155 &[],
156 ) {
157 return !matches!(v.to_lowercase().as_str(), "0" | "false" | "no" | "off");
158 }
159 // Auto: only top-tier models use compact; others (deepseek, qwen, 7b, ollama) get full prompt
160 let model = match model {
161 Some(m) => m.to_lowercase(),
162 None => return false, // unknown model → full
163 };
164 let compact_models = ["claude-4.6", "gpt-4.5", "gpt-5", "gemini-2.5", "gemini-3.0"];
165 compact_models
166 .iter()
167 .any(|p| model.starts_with(p) || model.contains(p))
168}