diff --git a/docs/public/agents/prompts.mdx b/docs/public/agents/prompts.mdx index 65071ee1d..50abbe3d1 100644 --- a/docs/public/agents/prompts.mdx +++ b/docs/public/agents/prompts.mdx @@ -145,6 +145,8 @@ The system prompt varies by LLM provider. Each provider has its own identity tex This is the full system prompt sent to Claude as the LLM system message. The `` block is filled in at runtime. +Tool guidance tracks the tools actually registered for the session. The `web_search` section shown below is present only when a [Brave Search API key](/integrations/brave-search) is configured; without one, both the tool and its guidance are omitted. + ``` You are Claude, an AI coding assistant made by Anthropic. You help users with software engineering tasks including solving bugs, adding new functionality, diff --git a/docs/public/integrations/brave-search.mdx b/docs/public/integrations/brave-search.mdx index 6a0b645ed..4ce0cfc9e 100644 --- a/docs/public/integrations/brave-search.mdx +++ b/docs/public/integrations/brave-search.mdx @@ -3,7 +3,7 @@ title: "Brave Search" description: "Give Fabro agents web search capabilities via the Brave Search API" --- -Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. The tool is registered automatically for all provider profiles (Anthropic, OpenAI, Gemini) — no workflow configuration is needed beyond setting the API key. +Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. Setting the API key is the only configuration needed — the tool is then registered for all provider profiles (Anthropic, OpenAI, Gemini). Without a key the tool is not registered at all, so agents are never offered a search tool they cannot use. ## Setup @@ -21,7 +21,7 @@ fabro secret set BRAVE_SEARCH_API_KEY BSA... fabro doctor ``` -The doctor output should show **Brave Search** as "connected". If the key is missing, web search is reported as a warning — workflows still run, but `web_search` calls return an error. +The doctor output should show **Brave Search** as "connected". If the key is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt, so agents fall back to other tools. The Fabro server reads this key from the vault only. It does not read `BRAVE_SEARCH_API_KEY` from process env or `server.env`. diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index 6bdaadd36..ee56389b3 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -15,7 +15,7 @@ use fabro_agent::profiles::assemble_system_prompt; use fabro_agent::tool_registry::ToolRegistry; use fabro_agent::{ AgentEvent, AgentProfile, AgentProfileBuilder, Error as AgentError, Session, SessionEvent, - SessionOptions, ToolSecrets, WebFetchSummarizer, + SessionOptions, WebFetchSummarizer, }; use fabro_api::types::{ CreateRunSessionRequest, PaginatedEventList, PaginationMeta, SubmitTurnRequest, @@ -731,20 +731,16 @@ async fn build_agent_session( .await .map_err(AskFabroBuildError::SandboxUnavailable)?; let sandbox: Arc = Arc::from(sandbox); - let brave_search_api_key = state - .vault_secret(EnvVars::BRAVE_SEARCH_API_KEY) - .await - .map_err(|err| AskFabroBuildError::Agent(anyhow::Error::new(err)))?; let summarizer = WebFetchSummarizer { client: llm_result.client.clone(), model_id: summarizer_model_id(&provider_id, profile_kind, &catalog, &model), }; + // No tool secrets: `AskFabroToolAccessPolicy` denies `web_search`, and both + // `tools()` and the prompt are filtered through that policy, so a Brave key + // here would only register a tool this session can never call. let mut profile = AgentProfileBuilder::new(profile_kind, provider_id, &model, Arc::clone(&catalog)) .with_web_fetch_summarizer(Some(summarizer)) - .with_tool_secrets(ToolSecrets { - brave_search_api_key, - }) .build(); // Give the Ask Fabro agent access to read-only run-inspection tools scoped diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index c2f3423ce..12e033e03 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -547,7 +547,7 @@ pub async fn run_with_args_and_client_and_catalog( client.clone(), ))) .with_tool_secrets(tool_secrets); - let mut profile = profile_builder.clone().build(); + let mut profile = profile_builder.build(); // Build sandbox let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); @@ -583,7 +583,7 @@ pub async fn run_with_args_and_client_and_catalog( let factory_hooks = config.tool_hooks.clone(); let factory_permission_level = config.permission_level; let factory: SessionFactory = Arc::new(move || { - let child_profile = factory_profile_builder.clone().build(); + let child_profile = factory_profile_builder.build(); let child_profile: Arc = Arc::from(child_profile); Session::new( factory_client.clone(), diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs index cb68ada66..30b168050 100644 --- a/lib/components/fabro-agent/src/config.rs +++ b/lib/components/fabro-agent/src/config.rs @@ -126,11 +126,19 @@ pub struct NativeToolOptions { impl NativeToolOptions { pub(crate) fn for_profile(profile_kind: AgentProfileKind) -> Self { - let mut options = Self::default(); - if profile_kind == AgentProfileKind::Anthropic { - options.default_command_timeout_ms = 120_000; + let defaults = Self::default(); + // Matched exhaustively so a new profile kind has to state its answer + // rather than silently inheriting the default timeout. + let default_command_timeout_ms = match profile_kind { + AgentProfileKind::Anthropic => 120_000, + AgentProfileKind::OpenAi | AgentProfileKind::Gemini => { + defaults.default_command_timeout_ms + } + }; + Self { + default_command_timeout_ms, + ..defaults } - options } } diff --git a/lib/components/fabro-agent/src/profiles/anthropic.rs b/lib/components/fabro-agent/src/profiles/anthropic.rs index ec7fb07e8..793fb11d1 100644 --- a/lib/components/fabro-agent/src/profiles/anthropic.rs +++ b/lib/components/fabro-agent/src/profiles/anthropic.rs @@ -13,24 +13,27 @@ use crate::todo_tools::{ make_task_create_tool, make_task_get_tool, make_task_list_tool, make_task_update_tool, }; use crate::tool_registry::ToolRegistry; -use crate::tools::{WebFetchSummarizer, make_edit_file_tool, register_core_tools}; +use crate::tools::{ + WEB_SEARCH_TOOL_NAME, WebFetchSummarizer, make_edit_file_tool, register_core_tools, +}; pub struct AnthropicProfile { base: BaseProfile, } fn anthropic_core_prompt(has_spawn_agent: bool, has_web_search: bool) -> String { + let using_tools = using_tools_section(has_web_search); let mut sections = vec![ - intro_section().to_string(), - system_section().to_string(), - "{env_block}".to_string(), - doing_tasks_section().to_string(), - executing_actions_section().to_string(), - using_tools_section(has_web_search), - session_specific_guidance_section(has_spawn_agent).to_string(), - communicating_with_user_section().to_string(), - tone_and_style_section().to_string(), - coding_best_practices_section().to_string(), + intro_section(), + system_section(), + "{env_block}", + doing_tasks_section(), + executing_actions_section(), + using_tools.as_str(), + session_specific_guidance_section(has_spawn_agent), + communicating_with_user_section(), + tone_and_style_section(), + coding_best_practices_section(), ]; sections.retain(|section| !section.is_empty()); sections.join("\n\n") @@ -180,16 +183,8 @@ in the project. Keep changes minimal and focused on the task." impl AnthropicProfile { #[must_use] pub fn new(model: impl Into) -> Self { - Self::with_summarizer(model, None) - } - - #[must_use] - pub fn with_summarizer( - model: impl Into, - summarizer: Option, - ) -> Self { let options = NativeToolOptions::for_profile(AgentProfileKind::Anthropic); - Self::with_native_tools(model, &options, summarizer) + Self::with_native_tools(model, &options, None) } pub(crate) fn with_native_tools( @@ -267,7 +262,7 @@ impl AgentProfile for AnthropicProfile { skills: &[Skill], ) -> String { let has_spawn_agent = self.base.registry.get("spawn_agent").is_some(); - let has_web_search = self.base.registry.get("web_search").is_some(); + let has_web_search = self.base.registry.get(WEB_SEARCH_TOOL_NAME).is_some(); let core_prompt = anthropic_core_prompt(has_spawn_agent, has_web_search); assemble_system_prompt( diff --git a/lib/components/fabro-agent/src/profiles/gemini.rs b/lib/components/fabro-agent/src/profiles/gemini.rs index 31f9e48ce..ea224dffa 100644 --- a/lib/components/fabro-agent/src/profiles/gemini.rs +++ b/lib/components/fabro-agent/src/profiles/gemini.rs @@ -10,8 +10,8 @@ use crate::sandbox::Sandbox; use crate::skills::Skill; use crate::tool_registry::ToolRegistry; use crate::tools::{ - WebFetchSummarizer, make_edit_file_tool, make_list_dir_tool, make_read_many_files_tool, - register_core_tools, + WEB_SEARCH_TOOL_NAME, WebFetchSummarizer, make_edit_file_tool, make_list_dir_tool, + make_read_many_files_tool, register_core_tools, }; pub struct GeminiProfile { @@ -21,16 +21,8 @@ pub struct GeminiProfile { impl GeminiProfile { #[must_use] pub fn new(model: impl Into) -> Self { - Self::with_summarizer(model, None) - } - - #[must_use] - pub fn with_summarizer( - model: impl Into, - summarizer: Option, - ) -> Self { let options = NativeToolOptions::for_profile(AgentProfileKind::Gemini); - Self::with_native_tools(model, &options, summarizer) + Self::with_native_tools(model, &options, None) } pub(crate) fn with_native_tools( @@ -103,7 +95,7 @@ impl AgentProfile for GeminiProfile { user_instructions: Option<&str>, skills: &[Skill], ) -> String { - let web_search_guidance = if self.base.registry.get("web_search").is_some() { + let web_search_guidance = if self.base.registry.get(WEB_SEARCH_TOOL_NAME).is_some() { "## web_search Search the web for information. @@ -111,8 +103,7 @@ Search the web for information. } else { "" }; - let core_prompt = format!( - "\ + let core_prompt = "\ You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks \ including solving bugs, adding new functionality, refactoring code, and explaining code. \ Your primary goal is to help users safely and effectively. @@ -144,7 +135,7 @@ still providing the best answer you can. files individually. - If you need to read multiple ranges in a file, do so in parallel. -{{env_block}} +{env_block} # Development Lifecycle @@ -201,7 +192,7 @@ Find files by name pattern. Results sorted by modification time. ## list_dir List directory contents with depth control. -{web_search_guidance}## web_fetch +{web_search_section}## web_fetch Fetch content from a URL and optionally summarize it. Pass a prompt to extract specific \ information instead of returning the full page. @@ -225,7 +216,7 @@ These are foundational mandates that take precedence over defaults in this promp Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \ in the project." - ); + .replace("{web_search_section}", web_search_guidance); assemble_system_prompt( &core_prompt, diff --git a/lib/components/fabro-agent/src/profiles/mod.rs b/lib/components/fabro-agent/src/profiles/mod.rs index 5e407d972..d78e681fd 100644 --- a/lib/components/fabro-agent/src/profiles/mod.rs +++ b/lib/components/fabro-agent/src/profiles/mod.rs @@ -20,8 +20,9 @@ use crate::tools::WebFetchSummarizer; /// Builds a provider profile and its native tools from one configuration. /// /// Native tool options must be supplied before [`Self::build`] because their -/// values are captured by tool executors during profile construction. Clone a -/// configured builder when root and child sessions must expose the same tools. +/// values are captured by tool executors during profile construction. +/// [`Self::build`] borrows, so one configured builder can outfit both a root +/// session and every child session it spawns with an identical tool set. #[derive(Clone)] pub struct AgentProfileBuilder { profile_kind: AgentProfileKind, @@ -56,17 +57,6 @@ impl AgentProfileBuilder { self } - #[must_use] - pub fn with_command_timeouts( - mut self, - default_command_timeout_ms: u64, - max_command_timeout_ms: u64, - ) -> Self { - self.native_tool_options.default_command_timeout_ms = default_command_timeout_ms; - self.native_tool_options.max_command_timeout_ms = max_command_timeout_ms; - self - } - #[must_use] pub fn with_web_fetch_summarizer(mut self, summarizer: Option) -> Self { self.summarizer = summarizer; @@ -74,34 +64,25 @@ impl AgentProfileBuilder { } #[must_use] - pub fn build(self) -> Box { + pub fn build(&self) -> Box { + let model = self.model.as_str(); + let options = &self.native_tool_options; + let summarizer = self.summarizer.clone(); match self.profile_kind { AgentProfileKind::OpenAi => Box::new( - OpenAiProfile::with_native_tools( - self.model, - &self.native_tool_options, - self.summarizer, - ) - .with_provider_id(self.provider_id) - .with_catalog(self.catalog), + OpenAiProfile::with_native_tools(model, options, summarizer) + .with_provider_id(self.provider_id.clone()) + .with_catalog(Arc::clone(&self.catalog)), ), AgentProfileKind::Gemini => Box::new( - GeminiProfile::with_native_tools( - self.model, - &self.native_tool_options, - self.summarizer, - ) - .with_provider_id(self.provider_id) - .with_catalog(self.catalog), + GeminiProfile::with_native_tools(model, options, summarizer) + .with_provider_id(self.provider_id.clone()) + .with_catalog(Arc::clone(&self.catalog)), ), AgentProfileKind::Anthropic => Box::new( - AnthropicProfile::with_native_tools( - self.model, - &self.native_tool_options, - self.summarizer, - ) - .with_provider_id(self.provider_id) - .with_catalog(self.catalog), + AnthropicProfile::with_native_tools(model, options, summarizer) + .with_provider_id(self.provider_id.clone()) + .with_catalog(Arc::clone(&self.catalog)), ), } } @@ -215,6 +196,7 @@ pub fn build_env_context_block_with(env: &dyn Sandbox, ctx: &EnvContext) -> Stri mod tests { use super::*; use crate::test_support::MockSandbox; + use crate::tools::WEB_SEARCH_TOOL_NAME; #[test] fn env_context_block_contains_platform() { @@ -279,7 +261,7 @@ mod tests { .build(); assert_eq!(profile.profile_kind(), profile_kind); assert_eq!(profile.provider_id(), provider_id); - assert!(profile.tool_registry().get("web_search").is_none()); + assert!(profile.tool_registry().get(WEB_SEARCH_TOOL_NAME).is_none()); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!( !prompt.contains("web_search"), @@ -294,13 +276,16 @@ mod tests { ) .with_tool_secrets(ToolSecrets { brave_search_api_key: Some("configured-key".to_string()), - }) - .with_command_timeouts(20_000, 600_000); - for configured in [ - configured_builder.clone().build(), - configured_builder.build(), - ] { - assert!(configured.tool_registry().get("web_search").is_some()); + }); + // Built twice: one configured builder must outfit both a root + // session and the child sessions it spawns. + for configured in [configured_builder.build(), configured_builder.build()] { + assert!( + configured + .tool_registry() + .get(WEB_SEARCH_TOOL_NAME) + .is_some() + ); let prompt = configured.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!( diff --git a/lib/components/fabro-agent/src/profiles/openai.rs b/lib/components/fabro-agent/src/profiles/openai.rs index 64898fc65..915192a5f 100644 --- a/lib/components/fabro-agent/src/profiles/openai.rs +++ b/lib/components/fabro-agent/src/profiles/openai.rs @@ -38,16 +38,8 @@ pub struct OpenAiProfile { impl OpenAiProfile { #[must_use] pub fn new(model: impl Into) -> Self { - Self::with_summarizer(model, None) - } - - #[must_use] - pub fn with_summarizer( - model: impl Into, - summarizer: Option, - ) -> Self { let options = NativeToolOptions::for_profile(AgentProfileKind::OpenAi); - Self::with_native_tools(model, &options, summarizer) + Self::with_native_tools(model, &options, None) } pub(crate) fn with_native_tools( @@ -196,7 +188,12 @@ The `old_string` must match exactly and be unique unless `replace_all` is true; surrounding context to make the match unique and preserve the existing indentation.", ), }; - let web_search_guidance = if self.base.registry.get("web_search").is_some() { + let web_search_guidance = if self + .base + .registry + .get(tools::WEB_SEARCH_TOOL_NAME) + .is_some() + { "## web_search Search the web using Brave Search. Returns titles, URLs, and descriptions. diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index 4c4b98c5e..5eca7b5be 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -46,6 +46,11 @@ fn html_to_markdown(text: &str) -> String { converter.convert(text).unwrap_or_else(|_| text.to_string()) } +/// Name of the Brave-backed web search tool. Profiles look this up in their own +/// registry to decide whether to advertise web search in the system prompt, so +/// availability and prompt guidance cannot drift apart. +pub const WEB_SEARCH_TOOL_NAME: &str = "web_search"; + /// Registers the core tools shared by all provider profiles: `read_file`, /// `write_file`, `shell`, `grep`, `glob`, and `web_fetch`. `web_search` is /// included when a Brave Search API key is configured. @@ -522,7 +527,7 @@ fn make_web_search_tool_with_api_key(api_key: String) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "web_search".into(), + name: WEB_SEARCH_TOOL_NAME.into(), description: "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.".into(), parameters: serde_json::json!({ "type": "object", diff --git a/lib/components/fabro-agent/tests/it/guardrails.rs b/lib/components/fabro-agent/tests/it/guardrails.rs index cae5461e3..0e6136473 100644 --- a/lib/components/fabro-agent/tests/it/guardrails.rs +++ b/lib/components/fabro-agent/tests/it/guardrails.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use fabro_agent::{AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile}; -use fabro_model::{Catalog, ProviderId}; +use fabro_agent::{AgentProfile, AgentProfileBuilder}; +use fabro_model::Catalog; #[test] fn profile_context_window_matches_catalog_for_default_models() { @@ -15,22 +15,13 @@ fn profile_context_window_matches_catalog_for_default_models() { let context_window = usize::try_from(catalog_info.context_window()) .expect("catalog context window should be non-negative and fit in usize"); - let profile: Box = match provider.agent_profile { - fabro_model::AgentProfileKind::OpenAi if provider.id == ProviderId::openai() => { - Box::new(OpenAiProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog))) - } - fabro_model::AgentProfileKind::OpenAi => Box::new( - OpenAiProfile::new(model.as_str()) - .with_provider_id(provider.id.clone()) - .with_catalog(Arc::clone(&catalog)), - ), - fabro_model::AgentProfileKind::Gemini => { - Box::new(GeminiProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog))) - } - fabro_model::AgentProfileKind::Anthropic => { - Box::new(AnthropicProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog))) - } - }; + let profile: Box = AgentProfileBuilder::new( + provider.agent_profile, + provider.id.clone(), + model.as_str(), + Arc::clone(&catalog), + ) + .build(); assert_eq!( profile.context_window_size(), diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index 03585fc93..72c55feb7 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -18,8 +18,8 @@ use fabro_llm::client::Client; use fabro_llm::provider::ProviderAdapter; use fabro_llm::providers::{OpenAiAdapter, OpenAiCompatibleAdapter}; use fabro_model::catalog::{LlmCatalogSettings, ProviderCatalogSettings}; -use fabro_model::{AgentProfileKind, Catalog, ModelHandle, ProviderId}; -use fabro_test::{TwinScenario, TwinScenarios, TwinToolCall, twin_openai}; +use fabro_model::{Catalog, ModelHandle, ProviderId}; +use fabro_test::{EnvVars, TwinScenario, TwinScenarios, TwinToolCall, twin_openai}; type Provider = ProviderId; @@ -54,15 +54,6 @@ fn build_summarizer(provider: &Provider, client: &Client) -> WebFetchSummarizer } } -fn profile_kind(provider: &Provider) -> AgentProfileKind { - match provider.as_str() { - ProviderId::ANTHROPIC => AgentProfileKind::Anthropic, - ProviderId::GEMINI => AgentProfileKind::Gemini, - ProviderId::OPENAI | "kimi" | "zai" | "minimax" | "inception" => AgentProfileKind::OpenAi, - other => panic!("unexpected provider {other}"), - } -} - fn profile_builder( provider: &Provider, model: &str, @@ -71,7 +62,12 @@ fn profile_builder( ) -> AgentProfileBuilder { let summarizer = Some(build_summarizer(provider, client)); let catalog = Arc::new(Catalog::from_builtin().expect("default catalog should build")); - AgentProfileBuilder::new(profile_kind(provider), provider.clone(), model, catalog) + // Ask the catalog rather than keeping a provider->profile list in the test, + // so adding a provider to the catalog cannot silently skip this matrix. + let profile_kind = catalog + .effective_agent_profile(provider, Some(model)) + .unwrap_or_else(|| panic!("no agent profile for provider {provider:?} in catalog")); + AgentProfileBuilder::new(profile_kind, provider.clone(), model, Arc::clone(&catalog)) .with_web_fetch_summarizer(summarizer) .with_tool_secrets(tool_secrets) } @@ -85,7 +81,7 @@ async fn make_session( ) -> Session { let client = make_client(&provider, twin.as_ref()).await; let profile_builder = profile_builder(&provider, model, &client, tool_secrets); - let mut profile = profile_builder.clone().build(); + let mut profile = profile_builder.build(); let env = Arc::new(LocalSandbox::new(cwd.to_path_buf())); // Register subagent tools so spawn_agent / wait / send_input / close_agent are @@ -95,7 +91,7 @@ async fn make_session( let factory_cwd = cwd.to_path_buf(); let factory_profile_builder = profile_builder; let factory: SessionFactory = Arc::new(move || { - let sub_profile: Arc = Arc::from(factory_profile_builder.clone().build()); + let sub_profile: Arc = Arc::from(factory_profile_builder.build()); let sub_env = Arc::new(LocalSandbox::new(factory_cwd.clone())); Session::new( factory_client.clone(), @@ -195,6 +191,17 @@ fn make_openai_compatible_twin_session( macro_rules! provider_test { ($scenario:ident, $provider:expr, $model:expr, $prefix:ident, keys = [$($key:expr),+ $(,)?]) => { + provider_test!( + $scenario, $provider, $model, $prefix, + keys = [$($key),+], + secrets = ToolSecrets::default() + ); + }; + ( + $scenario:ident, $provider:expr, $model:expr, $prefix:ident, + keys = [$($key:expr),+ $(,)?], + secrets = $secrets:expr + ) => { paste::paste! { #[fabro_macros::e2e_test($(live($key)),+)] async fn [<$prefix _ $scenario>]() { @@ -203,7 +210,7 @@ macro_rules! provider_test { $provider, $model, tmp.path(), - ToolSecrets::default(), + $secrets, None, ).await; session.initialize().await.unwrap(); @@ -213,24 +220,21 @@ macro_rules! provider_test { }; } +/// `web_search` is only registered when a Brave key is configured, so these +/// scenarios must supply one rather than relying on ambient env. macro_rules! web_search_provider_test { ($provider:expr, $model:expr, $prefix:ident, keys = [$($key:expr),+ $(,)?]) => { - paste::paste! { - #[fabro_macros::e2e_test($(live($key)),+)] - async fn [<$prefix _web_search>]() { - let tmp = tempfile::tempdir().expect("failed to create tempdir"); - let tool_secrets = ToolSecrets { - brave_search_api_key: Some( - std::env::var("BRAVE_SEARCH_API_KEY") - .expect("BRAVE_SEARCH_API_KEY must be set for web-search tests"), + provider_test!( + web_search, $provider, $model, $prefix, + keys = [$($key),+], + secrets = ToolSecrets { + brave_search_api_key: Some( + std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).expect( + "BRAVE_SEARCH_API_KEY must be set for web-search tests", ), - }; - let mut session = - make_session($provider, $model, tmp.path(), tool_secrets, None).await; - session.initialize().await.unwrap(); - scenario_web_search(&mut session, tmp.path()).await; + ), } - } + ); }; } diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index d89957563..b9d7a2b84 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -818,7 +818,7 @@ impl AgentApiBackend { Arc::clone(&catalog), ) .with_tool_secrets(tool_secrets); - let mut profile = profile_builder.clone().build(); + let mut profile = profile_builder.build(); let config = SessionOptions { max_tokens: node.max_tokens(), @@ -846,7 +846,7 @@ impl AgentApiBackend { let factory_fabro_run_tools = fabro_run_tools.clone(); let factory_permission_level = config.permission_level; let factory: SessionFactory = Arc::new(move || { - let mut child_profile = factory_profile_builder.clone().build(); + let mut child_profile = factory_profile_builder.build(); if let Some(services) = factory_fabro_run_tools.clone() { register_fabro_run_tools(child_profile.tool_registry_mut(), &services); }