refactor: simplify profile builder and drop dead tool plumbing

Follow-up cleanup on the profile-builder refactor.

AgentProfileBuilder::build now borrows instead of consuming, removing the
builder.clone().build() dance at all seven call sites. Deletes
with_command_timeouts, which had no caller but its own test, and the
with_summarizer constructors on all three profiles, whose only remaining
caller was each profile's own new().

Replaces the fifth copy of the profile-kind match (guardrails.rs) with the
builder, and swaps the parity matrix's hand-maintained provider list for
Catalog::effective_agent_profile so a new catalog provider cannot silently
skip the matrix. Collapses web_search_provider_test! into a secrets = arm
on provider_test! and uses EnvVars::BRAVE_SEARCH_API_KEY over a literal.

Drops the Brave key from the Ask Fabro session: AskFabroToolAccessPolicy
denies web_search, and both tools() and the prompt are filtered through
that policy, so the vault read only registered an uncallable tool.

Makes NativeToolOptions::for_profile match exhaustively so a new profile
kind must state its timeout, restores Anthropic's borrowed prompt sections
and Gemini's static prompt (placeholder substitution rather than format!
over 110 lines with doubled braces), and introduces WEB_SEARCH_TOOL_NAME
for the registry lookups that keep tool availability and prompt guidance
in sync.

Updates the product docs, which still described web_search as always
registered and as erroring at call time when unconfigured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-07-24 17:32:33 -04:00
parent e15defe4c5
commit 4ce57f8aae
No known key found for this signature in database
13 changed files with 131 additions and 157 deletions

View file

@ -145,6 +145,8 @@ The system prompt varies by LLM provider. Each provider has its own identity tex
<Accordion title="Example system prompt (Anthropic provider)">
This is the full system prompt sent to Claude as the LLM system message. The `<environment>` 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,

View file

@ -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`.

View file

@ -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<dyn fabro_agent::Sandbox> = 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

View file

@ -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<dyn AgentProfile> = Arc::from(child_profile);
Session::new(
factory_client.clone(),

View file

@ -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
}
}

View file

@ -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<String>) -> Self {
Self::with_summarizer(model, None)
}
#[must_use]
pub fn with_summarizer(
model: impl Into<String>,
summarizer: Option<WebFetchSummarizer>,
) -> 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(

View file

@ -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<String>) -> Self {
Self::with_summarizer(model, None)
}
#[must_use]
pub fn with_summarizer(
model: impl Into<String>,
summarizer: Option<WebFetchSummarizer>,
) -> 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,

View file

@ -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<WebFetchSummarizer>) -> Self {
self.summarizer = summarizer;
@ -74,34 +64,25 @@ impl AgentProfileBuilder {
}
#[must_use]
pub fn build(self) -> Box<dyn AgentProfile> {
pub fn build(&self) -> Box<dyn AgentProfile> {
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!(

View file

@ -38,16 +38,8 @@ pub struct OpenAiProfile {
impl OpenAiProfile {
#[must_use]
pub fn new(model: impl Into<String>) -> Self {
Self::with_summarizer(model, None)
}
#[must_use]
pub fn with_summarizer(
model: impl Into<String>,
summarizer: Option<WebFetchSummarizer>,
) -> 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.

View file

@ -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",

View file

@ -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<dyn AgentProfile> = 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<dyn AgentProfile> = AgentProfileBuilder::new(
provider.agent_profile,
provider.id.clone(),
model.as_str(),
Arc::clone(&catalog),
)
.build();
assert_eq!(
profile.context_window_size(),

View file

@ -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<dyn AgentProfile> = Arc::from(factory_profile_builder.clone().build());
let sub_profile: Arc<dyn AgentProfile> = 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;
),
}
}
);
};
}

View file

@ -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);
}