feat: Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts

This commit is contained in:
2026-06-18 13:01:11 -06:00
parent 241dda24f0
commit e77fa6ef42
3 changed files with 167 additions and 5 deletions
+1
View File
@@ -671,6 +671,7 @@ bitflags::bitflags! {
const SESSION = 1 << 2; const SESSION = 1 << 2;
const RAG = 1 << 3; const RAG = 1 << 3;
const AGENT = 1 << 4; const AGENT = 1 << 4;
const FUNCTION_CALLING = 1 << 5;
} }
} }
+154 -2
View File
@@ -371,6 +371,9 @@ impl RequestContext {
if self.rag.is_some() { if self.rag.is_some() {
flags |= StateFlags::RAG; flags |= StateFlags::RAG;
} }
if self.app.config.function_calling_support {
flags |= StateFlags::FUNCTION_CALLING;
}
flags flags
} }
@@ -450,6 +453,34 @@ impl RequestContext {
} }
} }
pub fn tools_info(&self) -> Result<String> {
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<String> { pub fn list_sessions(&self) -> Vec<String> {
list_file_names(self.sessions_dir(), ".yaml") list_file_names(self.sessions_dir(), ".yaml")
} }
@@ -4062,9 +4093,47 @@ mod tests {
} }
#[test] #[test]
fn state_empty_context() { fn state_empty_context_has_no_context_flags() {
let ctx = create_test_ctx(); 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] #[test]
@@ -4092,6 +4161,89 @@ mod tests {
assert!(state.contains(StateFlags::SESSION_EMPTY)); 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<usize> = 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] #[test]
fn role_info_errors_when_no_role() { fn role_info_errors_when_no_role() {
let ctx = create_test_ctx(); let ctx = create_test_ctx();
+12 -3
View File
@@ -49,10 +49,15 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
4. Continue with the next pending item now. Call tools immediately." 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(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", 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( ReplCommand::new(
".authenticate", ".authenticate",
"Authenticate the current model client via OAuth (if configured)", "Authenticate the current model client via OAuth (if configured)",
@@ -480,6 +485,10 @@ pub async fn run_repl_command(
let info = ctx.agent_info()?; let info = ctx.agent_info()?;
print!("{info}"); print!("{info}");
} }
Some("tools") => {
let info = ctx.tools_info()?;
print!("{info}");
}
Some(_) => unknown_command()?, Some(_) => unknown_command()?,
None => { None => {
let app = Arc::clone(&ctx.app.config); let app = Arc::clone(&ctx.app.config);
@@ -1382,8 +1391,8 @@ mod tests {
} }
#[test] #[test]
fn repl_commands_has_44_entries() { fn repl_commands_has_45_entries() {
assert_eq!(REPL_COMMANDS.len(), 44); assert_eq!(REPL_COMMANDS.len(), 45);
} }
#[test] #[test]