From e77fa6ef42f762ad7b65478ee95e8db21e7d31aa Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 18 Jun 2026 13:01:11 -0600 Subject: [PATCH] feat: Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts --- src/config/mod.rs | 1 + src/config/request_context.rs | 156 +++++++++++++++++++++++++++++++++- src/repl/mod.rs | 15 +++- 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 33a4ba3..be06846 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -671,6 +671,7 @@ bitflags::bitflags! { const SESSION = 1 << 2; const RAG = 1 << 3; const AGENT = 1 << 4; + const FUNCTION_CALLING = 1 << 5; } } diff --git a/src/config/request_context.rs b/src/config/request_context.rs index ad4aba2..b85d569 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -371,6 +371,9 @@ impl RequestContext { if self.rag.is_some() { flags |= StateFlags::RAG; } + if self.app.config.function_calling_support { + flags |= StateFlags::FUNCTION_CALLING; + } flags } @@ -450,6 +453,34 @@ impl RequestContext { } } + pub fn tools_info(&self) -> Result { + if !self.app.config.function_calling_support { + bail!( + "Function calling is disabled. Enable it by setting `function_calling_support: true` in your config or running `.set function_calling_support true`." + ); + } + let role = self.extract_role(&self.app.config)?; + match self.select_functions(&role) { + None => Ok("No tools enabled for the next request.\n".to_string()), + Some(functions) => { + let mut names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect(); + names.sort_unstable(); + let mut out = format!( + "Tools enabled for the next request: {}\n\n", + functions.len() + ); + + for name in names { + out.push_str(" "); + out.push_str(name); + out.push('\n'); + } + + Ok(out) + } + } + } + pub fn list_sessions(&self) -> Vec { list_file_names(self.sessions_dir(), ".yaml") } @@ -4062,9 +4093,47 @@ mod tests { } #[test] - fn state_empty_context() { + fn state_empty_context_has_no_context_flags() { let ctx = create_test_ctx(); - assert_eq!(ctx.state(), StateFlags::empty()); + + let state = ctx.state(); + + assert!(!state.contains(StateFlags::ROLE)); + assert!(!state.contains(StateFlags::SESSION)); + assert!(!state.contains(StateFlags::SESSION_EMPTY)); + assert!(!state.contains(StateFlags::AGENT)); + assert!(!state.contains(StateFlags::RAG)); + } + + #[test] + fn state_includes_function_calling_when_app_enables_it() { + let ctx = create_test_ctx(); + + assert!(ctx.state().contains(StateFlags::FUNCTION_CALLING)); + } + + #[test] + fn state_omits_function_calling_when_app_disables_it() { + let app_state = { + let config = AppConfig { + function_calling_support: false, + ..AppConfig::default() + }; + Arc::new(AppState { + config: Arc::new(config), + vault: Arc::new(Vault::default()), + mcp_factory: Arc::new(McpFactory::default()), + rag_cache: Arc::new(RagCache::default()), + mcp_config: None, + mcp_log_path: None, + mcp_registry: None, + functions: Functions::default(), + }) + }; + + let ctx = RequestContext::new(app_state, WorkingMode::Cmd); + + assert!(!ctx.state().contains(StateFlags::FUNCTION_CALLING)); } #[test] @@ -4092,6 +4161,89 @@ mod tests { assert!(state.contains(StateFlags::SESSION_EMPTY)); } + #[test] + fn tools_info_returns_message_when_no_tools_enabled() { + let ctx = create_test_ctx(); + + let info = ctx.tools_info().unwrap(); + + assert!( + info.contains("No tools enabled"), + "expected 'No tools enabled' message, got: {info}" + ); + } + + #[test] + fn tools_info_lists_enabled_tool_names_alphabetically() { + let mut ctx = create_test_ctx(); + ctx.tool_scope.functions.append_todo_functions(); + let mut role = Role::new("r", "p"); + role.set_enabled_tools(Some(vec!["all".to_string()])); + ctx.role = Some(role); + + let info = ctx.tools_info().unwrap(); + + assert!( + info.contains("Tools enabled for the next request:"), + "expected count line, got: {info}" + ); + assert!( + info.contains("todo__init"), + "expected todo__init in output, got: {info}" + ); + + let positions: Vec = info + .lines() + .filter(|line| line.trim().starts_with("todo__")) + .enumerate() + .map(|(i, _)| i) + .collect(); + assert!( + !positions.is_empty(), + "expected at least one todo__ entry, got: {info}" + ); + + let todo_lines: Vec<&str> = info + .lines() + .filter(|line| line.trim().starts_with("todo__")) + .collect(); + let mut sorted = todo_lines.clone(); + sorted.sort_unstable(); + assert_eq!( + todo_lines, sorted, + "expected todo__ entries to be alphabetically sorted, got: {todo_lines:?}" + ); + } + + #[test] + fn tools_info_errors_when_function_calling_disabled() { + let app_state = { + let config = AppConfig { + function_calling_support: false, + ..AppConfig::default() + }; + Arc::new(AppState { + config: Arc::new(config), + vault: Arc::new(Vault::default()), + mcp_factory: Arc::new(McpFactory::default()), + rag_cache: Arc::new(RagCache::default()), + mcp_config: None, + mcp_log_path: None, + mcp_registry: None, + functions: Functions::default(), + }) + }; + let ctx = RequestContext::new(app_state, WorkingMode::Cmd); + + let err = ctx.tools_info().unwrap_err(); + + let msg = err.to_string(); + assert!( + msg.contains("Function calling is disabled"), + "expected error to mention function calling, got: {msg}" + ); + } + #[test] fn role_info_errors_when_no_role() { let ctx = create_test_ctx(); diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 53dd33e..0dd4c26 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -49,10 +49,15 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {" 4. Continue with the next pending item now. Call tools immediately." }; -static REPL_COMMANDS: LazyLock<[ReplCommand; 44]> = LazyLock::new(|| { +static REPL_COMMANDS: LazyLock<[ReplCommand; 45]> = LazyLock::new(|| { [ ReplCommand::new(".help", "Show this help guide", AssertState::pass()), ReplCommand::new(".info", "Show system info", AssertState::pass()), + ReplCommand::new( + ".info tools", + "Show the list of enabled tools to be passed to the LLM", + AssertState::True(StateFlags::FUNCTION_CALLING), + ), ReplCommand::new( ".authenticate", "Authenticate the current model client via OAuth (if configured)", @@ -480,6 +485,10 @@ pub async fn run_repl_command( let info = ctx.agent_info()?; print!("{info}"); } + Some("tools") => { + let info = ctx.tools_info()?; + print!("{info}"); + } Some(_) => unknown_command()?, None => { let app = Arc::clone(&ctx.app.config); @@ -1382,8 +1391,8 @@ mod tests { } #[test] - fn repl_commands_has_44_entries() { - assert_eq!(REPL_COMMANDS.len(), 44); + fn repl_commands_has_45_entries() { + assert_eq!(REPL_COMMANDS.len(), 45); } #[test]