Tool whitelist from agent header filters native tools

The tools field in agent headers now filters which native tools
the agent receives. Empty = all tools (default). Non-empty =
whitelist. Journal agent can list only journal_tail/journal_new/
journal_update. Log shows actual tool names instead of "no tools".

Threaded tools list through call_api_with_tools → sync wrapper →
llm caller.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-01 15:18:42 -04:00
parent 834247fa53
commit 8eabeab8eb
3 changed files with 20 additions and 9 deletions

View file

@ -35,6 +35,7 @@ pub async fn call_api_with_tools(
prompts: &[String],
phases: &[String],
temperature: Option<f32>,
tools: &[String],
bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
log: &dyn Fn(&str),
) -> Result<String, String> {
@ -43,10 +44,16 @@ pub async fn call_api_with_tools(
// Set up a UI channel — we drain reasoning tokens into the log
let (ui_tx, mut ui_rx) = crate::agent::ui_channel::channel();
// Subconscious agents only get memory tools — no filesystem access.
// TODO: respect tools whitelist from agent header to filter which
// native tools each agent gets (e.g. journal agent only gets journal tools)
let tool_defs = thought::memory_and_journal_definitions();
// All available native tools for subconscious agents
let all_tools = thought::memory_and_journal_definitions();
// If agent header specifies a tools whitelist, filter to only those
let tool_defs: Vec<_> = if tools.is_empty() {
all_tools
} else {
all_tools.into_iter()
.filter(|t| tools.iter().any(|w| w == &t.function.name))
.collect()
};
let tracker = ProcessTracker::new();
// Provenance tracks which agent:phase is making writes.
// Updated between steps by the bail function via set_provenance().
@ -226,6 +233,7 @@ pub fn call_api_with_tools_sync(
prompts: &[String],
phases: &[String],
temperature: Option<f32>,
tools: &[String],
bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
log: &(dyn Fn(&str) + Sync),
) -> Result<String, String> {
@ -236,7 +244,7 @@ pub fn call_api_with_tools_sync(
.build()
.map_err(|e| format!("tokio runtime: {}", e))?;
rt.block_on(
call_api_with_tools(agent, prompts, phases, temperature, bail_fn, log)
call_api_with_tools(agent, prompts, phases, temperature, tools, bail_fn, log)
)
}).join().unwrap()
})

View file

@ -294,8 +294,11 @@ fn run_one_agent_inner(
_llm_tag: &str,
log: &(dyn Fn(&str) + Sync),
) -> Result<AgentResult, String> {
let tools_desc = if def.tools.is_empty() { "no tools".into() }
else { format!("{} tools", def.tools.len()) };
let tools_desc = if def.tools.is_empty() {
"all tools".into()
} else {
def.tools.join(", ")
};
let n_steps = agent_batch.steps.len();
for key in &agent_batch.node_keys {

View file

@ -22,7 +22,7 @@ pub(crate) fn call_simple(caller: &str, prompt: &str) -> Result<String, String>
let prompts = vec![prompt.to_string()];
let phases = vec![];
super::api::call_api_with_tools_sync(caller, &prompts, &phases, None, None, &log)
super::api::call_api_with_tools_sync(caller, &prompts, &phases, None, &[], None, &log)
}
/// Call a model using an agent definition's configuration (multi-step).
@ -34,7 +34,7 @@ pub(crate) fn call_for_def_multi(
bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>,
log: &(dyn Fn(&str) + Sync),
) -> Result<String, String> {
super::api::call_api_with_tools_sync(&def.agent, prompts, phases, def.temperature, bail_fn, log)
super::api::call_api_with_tools_sync(&def.agent, prompts, phases, def.temperature, &def.tools, bail_fn, log)
}