diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 503dc1f..8c52b29 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -22,6 +22,7 @@ use rustyline::Editor; use tokio::sync::Mutex; use tinyharness_lib::{ + config::load_merged_settings, config::load_settings, mode::AgentMode, provider::{Message, Provider, Role}, @@ -314,8 +315,9 @@ pub async fn run_agent_loop( let mut auto_accept = false; loop { - // Filter tools based on current mode - let tools = tool_manager.tools_for_mode(ctx.current_mode); + // Filter tools based on current mode and settings + let (_, _, merged) = load_merged_settings(); + let tools = tool_manager.tools_for_mode(ctx.current_mode, merged.auto_compact_enabled); // Call the provider — it returns a receiver for streaming chunks let mut recv = { diff --git a/src/agent/tui_loop.rs b/src/agent/tui_loop.rs index 4b6e5ca..389dd94 100644 --- a/src/agent/tui_loop.rs +++ b/src/agent/tui_loop.rs @@ -19,6 +19,7 @@ use std::sync::{ use tokio::sync::Mutex; use tinyharness_lib::{ + config::load_merged_settings, config::load_settings, provider::{Message, Provider, Role}, session::Session, @@ -457,8 +458,9 @@ async fn process_user_message( // Clear interrupt flag for this turn interrupted.store(false, Ordering::SeqCst); - // Filter tools based on current mode - let tools = tool_manager.tools_for_mode(ctx.current_mode); + // Filter tools based on current mode and settings + let (_, _, merged) = load_merged_settings(); + let tools = tool_manager.tools_for_mode(ctx.current_mode, merged.auto_compact_enabled); // Call the provider let mut recv = { diff --git a/src/commands/config_settings.rs b/src/commands/config_settings.rs index f631a6b..610752b 100644 --- a/src/commands/config_settings.rs +++ b/src/commands/config_settings.rs @@ -185,6 +185,45 @@ pub fn execute_autoaccept(out: &mut Output, arg: Option<&str>) -> Result) -> Result { + let a = arg.unwrap_or(""); + + if a.is_empty() { + let settings = load_settings(); + let (status, color) = if settings.auto_compact_enabled { + ("on", GREEN) + } else { + ("off", ORANGE) + }; + let _ = writeln!(out, "{BOLD}Auto-compact: {color}{status}{RESET}",); + return Ok(CommandResult::Ok); + } + + let new_value = match a.to_lowercase().as_str() { + "on" | "true" | "yes" | "1" => true, + "off" | "false" | "no" | "0" => false, + _ => { + return Err("Invalid value. Use 'on' or 'off', e.g. /autocompact off".to_string()); + } + }; + + let mut settings = load_settings(); + settings.auto_compact_enabled = new_value; + save_settings(&settings); + + let (status, color) = if new_value { + ("on", GREEN) + } else { + ("off", ORANGE) + }; + let _ = writeln!(out, "{BOLD}Auto-compact set to {color}{status}{RESET}",); + + Ok(CommandResult::Ok) +} + // ── Think Type (async — needs provider.lock().await) ─────────────────────── async_command!( diff --git a/src/commands/debug.rs b/src/commands/debug.rs index dab726d..ebb726a 100644 --- a/src/commands/debug.rs +++ b/src/commands/debug.rs @@ -430,6 +430,12 @@ fn dump_command_lists(file: &mut std::fs::File) { let denied: Vec = settings.denied_command_prefixes.clone().unwrap_or_default(); writeln!(file, "Auto-accept mode: {}", settings.auto_accept_mode).unwrap(); + writeln!( + file, + "Auto-compact enabled: {}", + settings.auto_compact_enabled + ) + .unwrap(); writeln!(file, "Safe command prefixes ({}):", safe.len()).unwrap(); for cmd in &safe { writeln!(file, " - {}", cmd).unwrap(); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 423f073..d6890ee 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -136,6 +136,15 @@ pub fn build_registry() -> CommandRegistry { |arg, ctx, _msg| crate::commands::config_settings::execute_autoaccept(&mut ctx.output, arg), ); + reg.register_sync_with_usage( + "/autocompact", + "Show or toggle the auto_compact tool (on/off). When off, the model cannot request conversation compaction.", + "/autocompact [on|off]", + |arg, ctx, _msg| { + crate::commands::config_settings::execute_autocompact(&mut ctx.output, arg) + }, + ); + // ── Per-project settings ────────────────────────────────────────────── reg.register_sync_with_usage( @@ -434,6 +443,7 @@ pub fn build_registry() -> CommandRegistry { reg.register_subcommands("/mode", vec!["agent", "casual", "planning", "research"]); reg.register_subcommands("/settings", vec!["all"]); reg.register_subcommands("/autoaccept", vec!["off", "safe", "all"]); + reg.register_subcommands("/autocompact", vec!["off", "on"]); reg.register_subcommands("/apikey", vec!["clear"]); reg.register_subcommands("/showthink", vec!["off", "on"]); reg.register_subcommands("/think", vec!["high", "low", "medium", "off"]); diff --git a/src/commands/project_settings.rs b/src/commands/project_settings.rs index 8562e29..39b8942 100644 --- a/src/commands/project_settings.rs +++ b/src/commands/project_settings.rs @@ -102,6 +102,15 @@ fn execute_show(out: &mut Output) { ); } + // ── Auto-compact ── + let ac_val = if merged.auto_compact_enabled { + "on" + } else { + "off" + }; + let (ac_str, ac_src) = format_setting(ac_val, merged.auto_compact_enabled_source, None); + let _ = writeln!(out, "{BOLD}│{RESET} Auto-Compact: {ac_str} {ac_src}"); + let _ = writeln!( out, "{BOLD}╰─────────────────────────────────────────────────╯{RESET}", @@ -193,7 +202,11 @@ fn execute_init(out: &mut Output) { // Preferred agent mode for this project. // Valid modes: casual, planning, agent, research - // "preferred_mode": "agent" + // "preferred_mode": "agent", + + // Enable or disable the auto_compact tool for this project. + // When false, the model will not see auto_compact as an available tool. + // "auto_compact_enabled": true } "#; diff --git a/src/commands/settings.rs b/src/commands/settings.rs index 653e288..a6cf962 100644 --- a/src/commands/settings.rs +++ b/src/commands/settings.rs @@ -92,6 +92,16 @@ fn execute_summary(out: &mut Output, settings: &tinyharness_lib::config::Setting "{BOLD}│{RESET} Auto-Accept: {auto_accept_color}{auto_accept_str}{RESET}", ); + let (ac_str, ac_color) = if settings.auto_compact_enabled { + ("on", GREEN) + } else { + ("off", ORANGE) + }; + let _ = writeln!( + out, + "{BOLD}│{RESET} Auto-Compact: {ac_color}{ac_str}{RESET}", + ); + let safe_commands = settings.get_safe_commands(); let denied_commands = settings.get_denied_commands(); let _ = writeln!( diff --git a/tinyharness-lib/src/config/mod.rs b/tinyharness-lib/src/config/mod.rs index de56440..2d750b0 100644 --- a/tinyharness-lib/src/config/mod.rs +++ b/tinyharness-lib/src/config/mod.rs @@ -122,6 +122,9 @@ pub struct ProjectSettings { /// Override the preferred mode for this project #[serde(default, skip_serializing_if = "Option::is_none")] pub preferred_mode: Option, + /// Override auto-compact enabled setting for this project + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auto_compact_enabled: Option, } /// Discover and load `.tinyharness/config.json` by walking up from CWD. @@ -198,6 +201,8 @@ pub struct MergedSettings { pub project_md_files_source: SettingSource, pub preferred_mode: AgentMode, pub preferred_mode_source: SettingSource, + pub auto_compact_enabled: bool, + pub auto_compact_enabled_source: SettingSource, } /// Load and merge global + project settings. @@ -243,6 +248,8 @@ fn merge_settings(global: &Settings, project: Option<&ProjectSettings>) -> Merge project_md_files_source: SettingSource::Default, preferred_mode: global.preferred_mode, preferred_mode_source: SettingSource::Default, + auto_compact_enabled: global.auto_compact_enabled, + auto_compact_enabled_source: SettingSource::Default, }, Some(p) => { // Safe commands: project extends global @@ -292,6 +299,11 @@ fn merge_settings(global: &Settings, project: Option<&ProjectSettings>) -> Merge .map(|m| (m, SettingSource::Project)) .unwrap_or((global.preferred_mode, SettingSource::Default)); + let (auto_compact_enabled, ac_source) = p + .auto_compact_enabled + .map(|v| (v, SettingSource::Project)) + .unwrap_or((global.auto_compact_enabled, SettingSource::Default)); + MergedSettings { safe_commands, safe_commands_source: safe_source, @@ -305,6 +317,8 @@ fn merge_settings(global: &Settings, project: Option<&ProjectSettings>) -> Merge project_md_files_source: md_source, preferred_mode, preferred_mode_source: mode_source, + auto_compact_enabled, + auto_compact_enabled_source: ac_source, } } } @@ -321,6 +335,7 @@ pub fn generate_project_config_template(settings: &Settings) -> ProjectSettings context_limit: settings.context_limit, project_md_files: None, // user must fill this in preferred_mode: Some(settings.preferred_mode), + auto_compact_enabled: Some(settings.auto_compact_enabled), } } @@ -489,6 +504,15 @@ pub struct Settings { /// Use `TINYHARNESS_MD_FILES` env var for the highest priority override. /// (default: None → use hardcoded defaults) pub project_md_files: Option>, + /// Enable auto-compact tool (default: true). + /// When false, the auto_compact tool is not registered with the provider, + /// so the model cannot request conversation compaction. + #[serde(default = "default_true")] + pub auto_compact_enabled: bool, +} + +fn default_true() -> bool { + true } impl Default for Settings { @@ -513,6 +537,7 @@ impl Default for Settings { safe_command_prefixes: None, denied_command_prefixes: None, project_md_files: None, + auto_compact_enabled: true, } } } diff --git a/tinyharness-lib/src/tools/mod.rs b/tinyharness-lib/src/tools/mod.rs index 81228e8..81a43f1 100644 --- a/tinyharness-lib/src/tools/mod.rs +++ b/tinyharness-lib/src/tools/mod.rs @@ -74,20 +74,39 @@ impl ToolManager { } /// Returns the tool definitions appropriate for the given agent mode. - pub fn tools_for_mode(&self, mode: AgentMode) -> Vec { + /// When `auto_compact_enabled` is false, the `auto_compact` tool is excluded. + pub fn tools_for_mode( + &self, + mode: AgentMode, + auto_compact_enabled: bool, + ) -> Vec { + let filter_compact = |t: &&Tool| { + if t.name == "auto_compact" { + auto_compact_enabled + } else { + true + } + }; match mode { - AgentMode::Agent => self.get_all_tool_definitions(), + AgentMode::Agent => self + .tools + .iter() + .filter(filter_compact) + .map(|t| t.to_definition()) + .collect(), AgentMode::Casual => self .tools .iter() - .filter(|t| t.name == "web_search" || t.name == "web_fetch") + .filter(|t| filter_compact(t) && (t.name == "web_search" || t.name == "web_fetch")) .map(|t| t.to_definition()) .collect(), AgentMode::Planning => self .tools .iter() .filter(|t| { - t.category == ToolCategory::ReadOnly || t.category == ToolCategory::Signal + filter_compact(t) + && (t.category == ToolCategory::ReadOnly + || t.category == ToolCategory::Signal) }) .map(|t| t.to_definition()) .collect(), @@ -95,7 +114,9 @@ impl ToolManager { .tools .iter() .filter(|t| { - t.category == ToolCategory::ReadOnly || t.category == ToolCategory::Signal + filter_compact(t) + && (t.category == ToolCategory::ReadOnly + || t.category == ToolCategory::Signal) }) .map(|t| t.to_definition()) .collect(),