This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 11d2f7e95205 CAMEL-24834: camel-jbang - load AI tool groups (SQL, 
tracing, resilience) from what the selected integration has (#27318)
11d2f7e95205 is described below

commit 11d2f7e952051509635af474f1dc69263bb3e084
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 4 16:34:40 2026 +0200

    CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) 
from what the selected integration has (#27318)
    
    * CAMEL-24834: load AI tool groups (SQL, tracing, resilience) from what the 
selected app has, read-only SQL for local models
    
    The runtime status of an integration now tells which tool groups a small 
model needs: AppFeatures reads the
    datasources, SQL endpoints, circuit breakers, OpenTelemetry, message 
tracing and Micrometer from the status file,
    ToolGroups picks the sql, tracing and resilience groups with one line of 
guidance each, and SqlReadOnlyGuard
    refuses SQL that writes.
    
    - shared registry: query_sql (read-only SQL) and get_tool_groups
    - camel-jbang-mcp: camel_runtime_sql_query and camel_runtime_tool_groups, 
both read-only
    - AI panel in camel-jbang views: the core tool set gets the groups of the 
selected integration, read again only when
      another integration is selected or it reloads; SQL is read-only unless 
camel.tui.ai.sqlWrites=true
    - docs for the tool groups, the new MCP tools and the setting
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-24834: SQL in the tool group is not limited to reading (developer 
tool)
    
    The AI panel and the MCP server are developer tools, not meant to reach 
production databases, so the sql tool
    group offers the SQL tools as they are: camel_runtime_sql, 
camel_runtime_datasources and camel_runtime_sql_trace
    over MCP, tui_execute_sql and tui_update_row in the AI panel. Removes 
SqlReadOnlyGuard, query_sql /
    camel_runtime_sql_query, the sqlWrites argument and setting, and the SQL 
mode in the fingerprint.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-24834: review - newer features win on merge, consistent volatile 
fields, broader status read guard
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  38 +++
 .../modules/ROOT/pages/camel-jbang-tui-ai.adoc     |   2 +-
 .../ROOT/pages/camel-jbang-tui-local-models.adoc   |  26 ++
 .../dsl/jbang/core/commands/ai/AppFeatures.java    | 284 +++++++++++++++++++++
 .../dsl/jbang/core/commands/ai/ToolGroup.java      |  42 +++
 .../dsl/jbang/core/commands/ai/ToolGroups.java     | 178 +++++++++++++
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  53 ++++
 .../jbang/core/commands/ai/AppFeaturesTest.java    | 132 ++++++++++
 .../dsl/jbang/core/commands/ai/ToolGroupsTest.java | 114 +++++++++
 .../jbang/core/commands/ai/ToolRegistryTest.java   |  33 +++
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  11 +
 .../jbang/core/commands/mcp/RuntimeToolsTest.java  |  14 +-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 103 +++++++-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  22 ++
 .../dsl/jbang/core/commands/tui/TuiToolGroups.java | 106 ++++++++
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  11 +
 .../core/commands/tui/AiPanelPromptBudgetTest.java |  48 ++++
 .../core/commands/tui/AiPanelToolGroupsTest.java   | 176 +++++++++++++
 18 files changed, 1387 insertions(+), 6 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 9ba4fd6371a6..1586b3770178 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -798,6 +798,10 @@ process is running).
   the update count otherwise. Lets the model look at the data a route reads or 
writes, or try a statement
   before putting it in a route.
 
+| `camel_runtime_tool_groups`
+| Which runtime tool groups (`sql`, `tracing`, `resilience`) the application 
needs, from what its status shows.
+  See <<_tool_groups_for_local_models>>.
+
 | `camel_runtime_datasources`
 | Datasource connection pool status: active, idle and total connections, max 
pool size and waiting threads.
 
@@ -827,6 +831,40 @@ process is running).
 | Compares the running route definitions with the source files to detect 
configuration drift.
 |===
 
+==== Tool groups for local models
+
+A client that drives a small local model pays for every tool schema on every 
request. Rather than listing every
+runtime tool, it can offer the core tools plus the groups the selected 
application needs, which
+`camel_runtime_tool_groups` works out from the application's status:
+
+[cols="1,2,2",options="header"]
+|===
+| Group | Loads when the application has | Tools
+
+| `sql`
+| a datasource, a `sql`, `sql-stored`, `jdbc`, `spring-jdbc` or `jpa` 
endpoint, or traced SQL statements
+| `camel_runtime_sql`, `camel_runtime_datasources`, `camel_runtime_sql_trace`
+
+| `tracing`
+| OpenTelemetry, enabled message tracing, or Micrometer
+| `camel_runtime_trace`, plus `camel_runtime_spans` with OpenTelemetry and 
`camel_runtime_metrics` with Micrometer
+
+| `resilience`
+| a circuit breaker in a route, or circuit breakers in the Resilience4j or 
Fault Tolerance status
+| `camel_runtime_circuit_breakers`
+|===
+
+The answer names the application and its pid, lists the `core` tools (the 
shared `camel_*` tools a small model
+should always get), each loaded group with its tools and one line of guidance 
for the system prompt (for example
+`SQL: datasource(s) orders (HikariCP), used by sql endpoints. Table names come 
from the SQL trace
+(camel_runtime_sql_trace); don't guess a schema.`), and the `signals` in the 
status that loaded each group. Its
+`fingerprint` stays the same as long as the groups and the datasources do,
+so a client calls the tool when the user selects an application or the 
application reloads, and rebuilds its
+tool list and prompt only when the fingerprint changes; the prompt then stays 
the same and a local model keeps
+its prompt cache. A status written by an older Camel version may lack some of 
the keys, which just loads fewer
+groups. The Camel TUI does the same for its AI panel, see
+xref:camel-jbang-tui-local-models.adoc#_tool_groups_from_the_selected_integration[Tool
 groups from the selected integration].
+
 ==== Configuration and Registry
 
 [cols="1,3",options="header"]
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
index 66c647cce0ff..4dbfcf0f7314 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
@@ -64,7 +64,7 @@ cycles backward.
 | Show or switch how file writes by the model (`camel_write_file`) are 
handled. `confirm` (default) shows the confirm dialog for every write, whatever 
the model passes; `auto` lets the model skip the dialog with `confirm=false`; 
`live` replays the edit in the Source editor so you watch it happen (see 
below). The mode lasts for the session.
 
 | `/tools [auto\|core\|full]` (`/t`)
-| Show which tool set is sent to the model, or switch it. `auto` (default) 
sends the core set to local providers and every tool to hosted ones; the choice 
is saved as `camel.tui.ai.tools`.
+| Show which tool set is sent to the model, or switch it. `auto` (default) 
sends the core set to local providers and every tool to hosted ones; the choice 
is saved as `camel.tui.ai.tools`. With the core set it also shows the tool 
groups loaded for the selected integration, for example `core (26 of 60 tools), 
mode auto (local provider); groups: sql, resilience (from the selected 
integration)`, see 
xref:camel-jbang-tui-local-models.adoc#_tool_groups_from_the_selected_integration[Tool
 groups].
 
 | `/context` (`/ctx`)
 | Show what the next request costs: provider and model, tool set, static 
prefix size, history size and the session total, and with Ollama the context 
window, the prompt size above which the history is compacted and the last 
measured prompt. Useful with local models, where prompt size is time.
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
index 34e91c9360d8..acf8103abed2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
@@ -57,6 +57,32 @@ model loaded for 30 minutes and for a context window of 32k 
or 64k (see <<_worki
 `OLLAMA_CONTEXT_LENGTH` overrides it), so follow-up questions reuse the cached 
prompt instead of reloading
 the model.
 
+=== Tool groups from the selected integration
+
+On top of the core set, the panel loads the tool groups the selected 
integration needs, read from the status
+it writes while it runs:
+
+[cols="1,2,3",options="header"]
+|===
+| Group | Loads when the integration has | What the model gets
+| `sql` | a datasource, a `sql`, `sql-stored`, `jdbc`, `spring-jdbc` or `jpa` 
endpoint, or traced SQL statements
+| `tui_execute_sql` and `tui_update_row`, and a line naming the datasources 
and their pool; the table names come
+from the *SQL Trace* tab
+| `tracing` | OpenTelemetry, enabled message tracing, or Micrometer
+| a line pointing at `tui_get_spans`, `tui_get_history` and the *Metrics* tab, 
which are core tools already
+| `resilience` | a circuit breaker in a route, or circuit breakers in the 
Resilience4j or Fault Tolerance status
+| a line naming the routes with a circuit breaker and pointing at the *Circuit 
Breaker* tab
+|===
+
+Each loaded group adds one line at the end of the system prompt. The groups 
are read again only when you
+select another integration or the selected one reloads its routes (after a 
reload the groups only grow), so
+the tools and the prompt stay the same from question to question and Ollama 
keeps reusing the cached prompt.
+An integration without any group yet is read again on each question, since one 
that just started may not
+have written its status completely. With all three groups the static prefix 
grows by about 520 tokens.
+
+The full tool set (`/tools full` and hosted providers) is not affected by the 
groups. `/tools` and `/context`
+show the loaded groups.
+
 == Working with a local Ollama model
 
 A local model is not a slower version of a hosted one; it spends its time 
differently, and the TUI
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
new file mode 100644
index 000000000000..f72822e8774b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its 
datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the 
status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older 
Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources          the datasources in the registry, by name
+ * @param sqlComponents        the SQL components the endpoints and routes use 
(sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced            whether SQL statements have been traced
+ * @param circuitBreaker       whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry        whether OpenTelemetry tracing is on
+ * @param messageTracing       whether message tracing is enabled
+ * @param micrometer           whether Micrometer metrics are on
+ * @param signals              the status keys that gave each feature away, 
with what they said
+ */
+public record AppFeatures(
+        List<DataSource> dataSources,
+        List<String> sqlComponents,
+        boolean sqlTraced,
+        boolean circuitBreaker,
+        List<String> circuitBreakerRoutes,
+        boolean openTelemetry,
+        boolean messageTracing,
+        boolean micrometer,
+        Map<String, String> signals) {
+
+    /** The components whose endpoints talk SQL to a datasource. */
+    static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", 
"jdbc", "spring-jdbc", "jpa");
+
+    private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+    private static final List<String> CIRCUIT_BREAKER_SECTIONS
+            = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+    /**
+     * A datasource of the registry.
+     *
+     * @param name     the bean name
+     * @param poolType HikariCP, Agroal or Unknown, null when not known
+     */
+    public record DataSource(String name, String poolType) {
+    }
+
+    public AppFeatures {
+        dataSources = List.copyOf(dataSources);
+        sqlComponents = List.copyOf(sqlComponents);
+        circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+        signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+    }
+
+    /** No integration, or nothing to tell: no tool group loads. */
+    public static AppFeatures none() {
+        return new AppFeatures(List.of(), List.of(), false, false, List.of(), 
false, false, false, Map.of());
+    }
+
+    /** The names of the datasources. */
+    public List<String> dataSourceNames() {
+        return dataSources.stream().map(DataSource::name).toList();
+    }
+
+    /** Whether the integration works with a database: a datasource, a SQL 
endpoint or a traced statement. */
+    public boolean sql() {
+        return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+    }
+
+    /** Whether there is anything to trace or measure: OpenTelemetry, message 
tracing or Micrometer. */
+    public boolean tracing() {
+        return openTelemetry || messageTracing || micrometer;
+    }
+
+    /**
+     * Both feature sets together: what an integration had before a reload 
still counts after it, so the tools a model
+     * was given do not come and go while it works. Where both know the same 
datasource or signal, the other (newer) one
+     * wins: a pool type known only after the pool started is the one the 
guidance names.
+     */
+    public AppFeatures merge(AppFeatures other) {
+        if (other == null) {
+            return this;
+        }
+        Map<String, DataSource> ds = new TreeMap<>();
+        for (DataSource d : dataSources) {
+            ds.put(d.name(), d);
+        }
+        for (DataSource d : other.dataSources) {
+            ds.put(d.name(), d);
+        }
+        Set<String> components = new TreeSet<>(sqlComponents);
+        components.addAll(other.sqlComponents);
+        Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+        routes.addAll(other.circuitBreakerRoutes);
+        Map<String, String> sig = new LinkedHashMap<>(signals);
+        other.signals.forEach(sig::put);
+        return new AppFeatures(
+                new ArrayList<>(ds.values()), new ArrayList<>(components), 
sqlTraced || other.sqlTraced,
+                circuitBreaker || other.circuitBreaker, new 
ArrayList<>(routes),
+                openTelemetry || other.openTelemetry, messageTracing || 
other.messageTracing,
+                micrometer || other.micrometer, sig);
+    }
+
+    /**
+     * Reads the features from an integration's status document. Pure and 
defensive: a null document, a missing key or a
+     * value of an unexpected type yields fewer features.
+     */
+    public static AppFeatures fromStatus(JsonObject status) {
+        if (status == null) {
+            return none();
+        }
+        Map<String, String> signals = new LinkedHashMap<>();
+
+        // datasources: dataSources.dataSources[] with name and poolType
+        Map<String, DataSource> dataSources = new TreeMap<>();
+        for (Map<?, ?> entry : objects(section(status, 
"dataSources").get("dataSources"))) {
+            String name = text(entry.get("name"));
+            if (name != null) {
+                dataSources.put(name, new DataSource(name, 
text(entry.get("poolType"))));
+            }
+        }
+        if (!dataSources.isEmpty()) {
+            signals.put("dataSources", String.join(",", dataSources.keySet()));
+        }
+
+        // SQL endpoints: the endpoint registry, the route inputs and the 
processors that send somewhere
+        Set<String> components = new TreeSet<>();
+        for (Map<?, ?> ep : objects(section(status, 
"endpoints").get("endpoints"))) {
+            addSqlComponent(components, text(ep.get("uri")));
+        }
+        List<Map<?, ?>> routes = objects(status.get("routes"));
+        Set<String> breakerRoutes = new TreeSet<>();
+        boolean breakerProcessor = false;
+        for (Map<?, ?> route : routes) {
+            addSqlComponent(components, text(route.get("from")));
+            for (Map<?, ?> p : objects(route.get("processors"))) {
+                addSqlComponent(components, text(p.get("uri")));
+                if 
(CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+                    breakerProcessor = true;
+                    String routeId = text(p.get("routeId"));
+                    if (routeId == null) {
+                        routeId = text(route.get("routeId"));
+                    }
+                    if (routeId != null) {
+                        breakerRoutes.add(routeId);
+                    }
+                }
+            }
+        }
+        List<Map<?, ?>> statements = objects(section(status, 
"sqlTrace").get("statements"));
+        for (Map<?, ?> st : statements) {
+            addSqlComponent(components, text(st.get("endpoint")));
+        }
+        if (!components.isEmpty()) {
+            signals.put("endpoints", String.join(",", components));
+        }
+        boolean sqlTraced = false;
+        for (Map<?, ?> st : statements) {
+            if (text(st.get("query")) != null) {
+                sqlTraced = true;
+                break;
+            }
+        }
+        if (sqlTraced) {
+            signals.put("sqlTrace", statements.size() + " statement(s)");
+        }
+
+        // circuit breakers: the circuitBreaker EIP in a route, or the 
breakers a console reports
+        if (breakerProcessor) {
+            String where = breakerRoutes.isEmpty() ? "" : " in " + 
String.join(",", breakerRoutes);
+            signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + 
where);
+        }
+        boolean breakers = false;
+        for (String key : CIRCUIT_BREAKER_SECTIONS) {
+            List<Map<?, ?>> list = objects(section(status, 
key).get("circuitBreakers"));
+            if (!list.isEmpty()) {
+                breakers = true;
+                signals.put(key, list.size() + " circuit breaker(s)");
+                for (Map<?, ?> b : list) {
+                    String routeId = text(b.get("routeId"));
+                    if (routeId != null) {
+                        breakerRoutes.add(routeId);
+                    }
+                }
+            }
+        }
+
+        // tracing and metrics
+        Set<String> consoles = new TreeSet<>();
+        for (Object id : list(status.get("devConsoles"))) {
+            if (id != null) {
+                consoles.add(id.toString());
+            }
+        }
+        boolean otel = consoles.contains("opentelemetry");
+        boolean micrometer = consoles.contains("micrometer") || 
status.get("micrometer") instanceof Map;
+        if (otel || consoles.contains("micrometer")) {
+            List<String> fired = new ArrayList<>();
+            if (otel) {
+                fired.add("opentelemetry");
+            }
+            if (consoles.contains("micrometer")) {
+                fired.add("micrometer");
+            }
+            signals.put("devConsoles", String.join(",", fired));
+        }
+        if (status.get("micrometer") instanceof Map) {
+            signals.put("micrometer", "present");
+        }
+        boolean tracing = Boolean.TRUE.equals(section(status, 
"trace").get("enabled"))
+                || "true".equals(text(section(status, 
"trace").get("enabled")));
+        if (tracing) {
+            signals.put("trace.enabled", "true");
+        }
+
+        return new AppFeatures(
+                new ArrayList<>(dataSources.values()), new 
ArrayList<>(components), sqlTraced,
+                breakerProcessor || breakers, new ArrayList<>(breakerRoutes), 
otel, tracing, micrometer, signals);
+    }
+
+    private static void addSqlComponent(Set<String> components, String uri) {
+        if (uri == null) {
+            return;
+        }
+        int colon = uri.indexOf(':');
+        if (colon > 0) {
+            String scheme = uri.substring(0, colon);
+            if (SQL_COMPONENTS.contains(scheme)) {
+                components.add(scheme);
+            }
+        }
+    }
+
+    private static Map<?, ?> section(Map<?, ?> root, String key) {
+        return root.get(key) instanceof Map<?, ?> m ? m : Map.of();
+    }
+
+    private static List<?> list(Object value) {
+        return value instanceof List<?> l ? l : List.of();
+    }
+
+    private static List<Map<?, ?>> objects(Object value) {
+        List<Map<?, ?>> answer = new ArrayList<>();
+        for (Object o : list(value)) {
+            if (o instanceof Map<?, ?> m) {
+                answer.add(m);
+            }
+        }
+        return answer;
+    }
+
+    private static String text(Object value) {
+        if (value == null) {
+            return null;
+        }
+        String s = value.toString();
+        return s.isBlank() ? null : s;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
new file mode 100644
index 000000000000..674fae926ac1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+/**
+ * A group of runtime tools that is only worth its schemas when the 
integration has what the tools read (CAMEL-24834): a
+ * small local model gets the core tools plus the groups of the selected 
integration, see {@link ToolGroups}.
+ */
+public enum ToolGroup {
+
+    /** Datasources, SQL queries and the SQL trace. */
+    SQL("sql"),
+    /** OpenTelemetry spans, message tracing and Micrometer metrics. */
+    TRACING("tracing"),
+    /** Circuit breakers. */
+    RESILIENCE("resilience");
+
+    private final String id;
+
+    ToolGroup(String id) {
+        this.id = id;
+    }
+
+    /** The id clients see, e.g. {@code sql}. */
+    public String id() {
+        return id;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
new file mode 100644
index 000000000000..ea1983dd63f4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Picks the runtime tool groups for an integration from its {@link 
AppFeatures} (CAMEL-24834). A small local model pays
+ * for every tool schema on every request, so the SQL, tracing and resilience 
tools only load when the integration has a
+ * database, tracing or circuit breakers; each loaded group adds one line of 
guidance that names what the integration
+ * has and which tool reads it. The tool names and the wording here are those 
of the MCP server; the TUI maps the same
+ * groups to its own tools.
+ */
+public final class ToolGroups {
+
+    public static final String SQL_TOOL = "camel_runtime_sql";
+    public static final String DATASOURCES_TOOL = "camel_runtime_datasources";
+    public static final String SQL_TRACE_TOOL = "camel_runtime_sql_trace";
+    public static final String SPANS_TOOL = "camel_runtime_spans";
+    public static final String TRACE_TOOL = "camel_runtime_trace";
+    public static final String METRICS_TOOL = "camel_runtime_metrics";
+    public static final String CIRCUIT_BREAKERS_TOOL = 
"camel_runtime_circuit_breakers";
+
+    /**
+     * A loaded group.
+     *
+     * @param group    the group
+     * @param tools    the tools it adds, in the order to offer them
+     * @param guidance one line for the model: what the integration has and 
which tool reads it
+     */
+    public record Group(ToolGroup group, List<String> tools, String guidance) {
+
+        public Group {
+            tools = List.copyOf(tools);
+        }
+    }
+
+    /**
+     * The groups for an integration.
+     *
+     * @param groups      the loaded groups, in {@link ToolGroup} order
+     * @param fingerprint stable for the same groups and datasources: a client 
rebuilds its tool list only when it
+     *                    changes
+     */
+    public record Selection(List<Group> groups, String fingerprint) {
+
+        public Selection {
+            groups = List.copyOf(groups);
+        }
+
+        public List<ToolGroup> toolGroups() {
+            return groups.stream().map(Group::group).toList();
+        }
+
+        public boolean has(ToolGroup group) {
+            return groups.stream().anyMatch(g -> g.group() == group);
+        }
+
+        /** The tools of all loaded groups. */
+        public List<String> mcpTools() {
+            return groups.stream().flatMap(g -> 
g.tools().stream()).distinct().toList();
+        }
+
+        /** The guidance lines of all loaded groups. */
+        public List<String> guidance() {
+            return groups.stream().map(Group::guidance).toList();
+        }
+    }
+
+    private ToolGroups() {
+    }
+
+    /** The groups an integration needs. */
+    public static Selection select(AppFeatures features) {
+        AppFeatures f = features != null ? features : AppFeatures.none();
+        List<Group> groups = new ArrayList<>();
+        for (ToolGroup group : groups(f)) {
+            switch (group) {
+                case SQL -> groups.add(new Group(
+                        group,
+                        List.of(SQL_TOOL, DATASOURCES_TOOL, SQL_TRACE_TOOL),
+                        "SQL: " + describeSql(f) + ". Table names come from 
the SQL trace (" + SQL_TRACE_TOOL
+                                                                             + 
"); don't guess a schema."));
+                case TRACING -> {
+                    List<String> tools = new ArrayList<>();
+                    List<String> parts = new ArrayList<>();
+                    if (f.openTelemetry()) {
+                        tools.add(SPANS_TOOL);
+                        parts.add("OpenTelemetry is on, " + SPANS_TOOL + " has 
the spans per trace");
+                    }
+                    tools.add(TRACE_TOOL);
+                    parts.add(f.messageTracing()
+                            ? "message tracing is on, " + TRACE_TOOL + " dump 
returns the traced messages"
+                            : TRACE_TOOL + " enables message tracing");
+                    if (f.micrometer()) {
+                        tools.add(METRICS_TOOL);
+                        parts.add(METRICS_TOOL + " has the Micrometer 
metrics");
+                    }
+                    groups.add(new Group(group, tools, "Tracing: " + 
String.join("; ", parts) + "."));
+                }
+                case RESILIENCE -> groups.add(new Group(
+                        group, List.of(CIRCUIT_BREAKERS_TOOL),
+                        describeBreakers(f) + ": " + CIRCUIT_BREAKERS_TOOL
+                                                               + " shows state 
(CLOSED/OPEN/HALF_OPEN) and failure rate;"
+                                                               + " OPEN means 
the fallback runs."));
+            }
+        }
+        return new Selection(groups, fingerprint(groups(f), f));
+    }
+
+    /** The groups the features call for, in {@link ToolGroup} order. */
+    public static List<ToolGroup> groups(AppFeatures features) {
+        List<ToolGroup> groups = new ArrayList<>();
+        if (features.sql()) {
+            groups.add(ToolGroup.SQL);
+        }
+        if (features.tracing()) {
+            groups.add(ToolGroup.TRACING);
+        }
+        if (features.circuitBreaker()) {
+            groups.add(ToolGroup.RESILIENCE);
+        }
+        return groups;
+    }
+
+    /**
+     * Sorted group ids and datasource names: the same integration gives the 
same fingerprint, whatever order its status
+     * lists things in.
+     */
+    static String fingerprint(List<ToolGroup> groups, AppFeatures features) {
+        String ids = 
groups.stream().map(ToolGroup::id).sorted().collect(Collectors.joining(","));
+        String ds = 
features.dataSourceNames().stream().sorted().collect(Collectors.joining(","));
+        return ids + "|" + ds;
+    }
+
+    /**
+     * The datasources with their pool, and the SQL components the routes use, 
e.g. {@code datasource(s) orders
+     * (HikariCP), used by sql endpoints}.
+     */
+    public static String describeSql(AppFeatures features) {
+        StringBuilder sb = new StringBuilder();
+        if (features.dataSources().isEmpty()) {
+            sb.append("no datasource listed yet");
+        } else {
+            sb.append("datasource(s) ").append(features.dataSources().stream()
+                    .map(d -> d.poolType() != null && 
!"Unknown".equals(d.poolType())
+                            ? d.name() + " (" + d.poolType() + ")" : d.name())
+                    .collect(Collectors.joining(", ")));
+        }
+        if (!features.sqlComponents().isEmpty()) {
+            sb.append(", used by ").append(String.join(", ", 
features.sqlComponents())).append(" endpoints");
+        }
+        return sb.toString();
+    }
+
+    /** {@code Circuit breakers in routes a, b}, or without the routes when 
they are not known. */
+    public static String describeBreakers(AppFeatures features) {
+        return features.circuitBreakerRoutes().isEmpty()
+                ? "Circuit breakers"
+                : "Circuit breakers in routes " + String.join(", ", 
features.circuitBreakerRoutes());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 7b6fdad92057..ad36c29da355 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -441,6 +441,22 @@ public final class ToolRegistry {
                     return result.toJson();
                 }));
 
+        register(tool("get_tool_groups",
+                "Which runtime tool groups (sql, tracing, resilience) the 
integration needs, from what it has: "
+                                         + "datasources and SQL endpoints, 
OpenTelemetry, message tracing, Micrometer, "
+                                         + "circuit breakers. Returns the 
tools of each group with one line of guidance, "
+                                         + "and a fingerprint that changes 
only when the groups do.")
+                .param("name", "string", "Name or PID of the integration 
(default: the selected or only one)", false)
+                .executor((ctx, args) -> {
+                    String name = args.get("name");
+                    if (name != null && !name.isBlank()) {
+                        ctx.selectProcess(name);
+                    } else {
+                        ctx.selectSingleProcessIfNone();
+                    }
+                    return toolGroups(ctx, ctx.readFullStatus()).toJson();
+                }));
+
         // Route control
         registerRouteControlTool("stop_route", "stop",
                 "Gracefully stop a route. The route will finish processing 
in-flight exchanges before stopping.");
@@ -455,6 +471,43 @@ public final class ToolRegistry {
                 .executor((ctx, args) -> ctx.stopApplication()));
     }
 
+    /**
+     * The answer of get_tool_groups: the integration, the core tools every 
client has, the groups its status calls for
+     * and the status keys that called for them.
+     */
+    static JsonObject toolGroups(ToolContext ctx, JsonObject status) {
+        AppFeatures features = AppFeatures.fromStatus(status);
+        ToolGroups.Selection selection = ToolGroups.select(features);
+        JsonObject answer = new JsonObject();
+        String app = null;
+        for (RuntimeHelper.ProcessInfo p : ctx.discoverProcesses()) {
+            if (p.pid() == ctx.pid()) {
+                app = p.name();
+            }
+        }
+        if (app == null && status != null && status.get("context") instanceof 
Map<?, ?> context
+                && context.get("name") != null) {
+            app = context.get("name").toString();
+        }
+        answer.put("app", app);
+        answer.put("pid", ctx.pid());
+        answer.put("fingerprint", selection.fingerprint());
+        JsonArray core = new JsonArray();
+        
authoringTools().stream().filter(ToolDescriptor::isCore).map(ToolDescriptor::name).forEach(core::add);
+        answer.put("core", core);
+        JsonArray groups = new JsonArray();
+        for (ToolGroups.Group g : selection.groups()) {
+            JsonObject group = new JsonObject();
+            group.put("id", g.group().id());
+            group.put("tools", new JsonArray(g.tools()));
+            group.put("guidance", g.guidance());
+            groups.add(group);
+        }
+        answer.put("groups", groups);
+        answer.put("signals", new JsonObject(features.signals()));
+        return answer;
+    }
+
     private static void registerRouteControlTool(String name, String command, 
String description) {
         register(tool(name, description)
                 .param("routeId", "string", "The ID of the route", true)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
new file mode 100644
index 000000000000..c49f9293998c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AppFeaturesTest {
+
+    private static AppFeatures features(String json) throws Exception {
+        return AppFeatures.fromStatus((JsonObject) 
Jsoner.deserialize(json.replace('\'', '"')));
+    }
+
+    @Test
+    void aDatasourceIsSql() throws Exception {
+        AppFeatures f = features("{'dataSources': {'dataSources': [{'name': 
'orders', 'type': 'com.zaxxer.hikari"
+                                 + ".HikariDataSource', 'poolType': 
'HikariCP'}]}}");
+        assertTrue(f.sql());
+        assertEquals(List.of(new AppFeatures.DataSource("orders", 
"HikariCP")), f.dataSources());
+        assertEquals("orders", f.signals().get("dataSources"));
+        assertFalse(f.tracing());
+        assertFalse(f.circuitBreaker());
+    }
+
+    @Test
+    void anEmptyDataSourcesArrayIsNotSql() throws Exception {
+        AppFeatures f = features("{'dataSources': {'dataSources': []}, 
'sqlTrace': {}}");
+        assertFalse(f.sql());
+        assertTrue(f.signals().isEmpty());
+        assertTrue(ToolGroups.groups(f).isEmpty());
+    }
+
+    @Test
+    void sqlEndpointsAndTracedStatementsAreSql() throws Exception {
+        AppFeatures f = features("{'endpoints': {'endpoints': [{'uri': 
'jdbc://default'}, {'uri': 'timer://tick'}]},"
+                                 + " 'routes': [{'routeId': 'r1', 'from': 
'sql:select * from orders'}],"
+                                 + " 'sqlTrace': {'statements': [{'query': 
'select * from orders',"
+                                 + " 'endpoint': 'sql-stored:proc'}]}}");
+        assertTrue(f.sql());
+        assertEquals(List.of("jdbc", "sql", "sql-stored"), f.sqlComponents());
+        assertTrue(f.sqlTraced());
+        assertTrue(f.dataSources().isEmpty());
+    }
+
+    @Test
+    void aCircuitBreakerProcessorNamesItsRoute() throws Exception {
+        AppFeatures f = features("{'routes': [{'routeId': 'pay', 'processors': 
[{'routeId': 'pay', 'id': 'cb1',"
+                                 + " 'processor': 'circuitBreaker'}, 
{'processor': 'to', 'uri': 'http://x'}]},"
+                                 + " {'routeId': 'audit', 'processors': 
[{'processor': 'log'}]}]}");
+        assertTrue(f.circuitBreaker());
+        assertEquals(List.of("pay"), f.circuitBreakerRoutes());
+        assertEquals("circuitBreaker in pay", 
f.signals().get("routes.processors"));
+    }
+
+    @Test
+    void aResilience4jSectionWithBreakersIsResilience() throws Exception {
+        AppFeatures f = features("{'resilience4j': {'circuitBreakers': 
[{'routeId': 'pay', 'id': 'cb1',"
+                                 + " 'state': 'OPEN'}]}, 'circuit-breaker': 
{'circuitBreakers': []}}");
+        assertTrue(f.circuitBreaker());
+        assertEquals(List.of("pay"), f.circuitBreakerRoutes());
+        assertEquals("1 circuit breaker(s)", f.signals().get("resilience4j"));
+        assertFalse(f.signals().containsKey("circuit-breaker"), "an empty 
section says nothing");
+    }
+
+    @Test
+    void theOpenTelemetryConsoleIsTracing() throws Exception {
+        AppFeatures f = features("{'devConsoles': ['context', 'route', 
'opentelemetry']}");
+        assertTrue(f.openTelemetry());
+        assertTrue(f.tracing());
+        assertFalse(f.micrometer());
+        assertEquals("opentelemetry", f.signals().get("devConsoles"));
+    }
+
+    @Test
+    void enabledMessageTracingIsTracing() throws Exception {
+        assertTrue(features("{'trace': {'enabled': true}}").messageTracing());
+        assertFalse(features("{'trace': {'enabled': false, 'standby': 
true}}").tracing(),
+                "the trace console is always there, only enabled tracing 
counts");
+    }
+
+    @Test
+    void micrometerIsTracing() throws Exception {
+        assertTrue(features("{'micrometer': {'counters': []}}").micrometer());
+        assertTrue(features("{'devConsoles': ['micrometer']}").micrometer());
+    }
+
+    @Test
+    void anOldStatusWithoutConsolesOrProcessorsGivesFewerFeatures() throws 
Exception {
+        // a status file of an older Camel: no devConsoles, routes without 
processors, odd types
+        AppFeatures f = features("{'context': {'name': 'old'}, 'routes': 
[{'routeId': 'r1', 'from': 'timer:x'}],"
+                                 + " 'dataSources': 'n/a', 'resilience4j': [], 
'trace': 'off'}");
+        assertEquals(AppFeatures.none().dataSources(), f.dataSources());
+        assertFalse(f.sql());
+        assertFalse(f.tracing());
+        assertFalse(f.circuitBreaker());
+        assertFalse(AppFeatures.fromStatus(null).sql());
+    }
+
+    @Test
+    void mergeKeepsWhatEitherHad() throws Exception {
+        AppFeatures before = features("{'dataSources': {'dataSources': 
[{'name': 'orders'}]},"
+                                      + " 'devConsoles': ['opentelemetry']}");
+        AppFeatures after = features("{'resilience4j': {'circuitBreakers': 
[{'routeId': 'pay'}]}}");
+        AppFeatures merged = before.merge(after);
+        assertTrue(merged.sql());
+        assertTrue(merged.openTelemetry());
+        assertTrue(merged.circuitBreaker());
+        assertEquals(List.of("orders"), merged.dataSourceNames());
+        assertEquals(merged, merged.merge(AppFeatures.none()));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
new file mode 100644
index 000000000000..c6400367830b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ToolGroupsTest {
+
+    static AppFeatures everything() {
+        return new AppFeatures(
+                List.of(new AppFeatures.DataSource("orders", "HikariCP"), new 
AppFeatures.DataSource("audit", null)),
+                List.of("sql"), true, true, List.of("pay", "ship"), true, 
true, true, Map.of());
+    }
+
+    @Test
+    void nothingLoadsNoGroup() {
+        ToolGroups.Selection s = ToolGroups.select(AppFeatures.none());
+        assertTrue(s.groups().isEmpty());
+        assertTrue(s.mcpTools().isEmpty());
+        assertTrue(s.guidance().isEmpty());
+        assertEquals("|", s.fingerprint());
+    }
+
+    @Test
+    void eachGroupHasItsTools() {
+        ToolGroups.Selection s = ToolGroups.select(everything());
+        assertEquals(List.of(ToolGroup.SQL, ToolGroup.TRACING, 
ToolGroup.RESILIENCE), s.toolGroups());
+        assertEquals(List.of("camel_runtime_sql", "camel_runtime_datasources", 
"camel_runtime_sql_trace"),
+                s.groups().get(0).tools());
+        assertEquals(List.of("camel_runtime_spans", "camel_runtime_trace", 
"camel_runtime_metrics"),
+                s.groups().get(1).tools());
+        assertEquals(List.of("camel_runtime_circuit_breakers"), 
s.groups().get(2).tools());
+    }
+
+    @Test
+    void micrometerAloneLoadsTheTracingGroupWithTheMetrics() {
+        AppFeatures micrometer = new AppFeatures(
+                List.of(), List.of(), false, false, List.of(), false, false, 
true, Map.of());
+        ToolGroups.Selection s = ToolGroups.select(micrometer);
+        assertEquals(List.of(ToolGroup.TRACING), s.toolGroups());
+        assertEquals(List.of("camel_runtime_trace", "camel_runtime_metrics"), 
s.groups().get(0).tools());
+        
assertTrue(s.groups().get(0).guidance().contains("camel_runtime_metrics has the 
Micrometer metrics"));
+    }
+
+    @Test
+    void theNewerFeaturesWinWhenMerged() {
+        AppFeatures before = new AppFeatures(
+                List.of(new AppFeatures.DataSource("orders", null)), 
List.of(), false, false, List.of(), false, false,
+                false, Map.of("dataSources", "orders"));
+        AppFeatures after = new AppFeatures(
+                List.of(new AppFeatures.DataSource("orders", "HikariCP")), 
List.of(), false, false, List.of(), false, false,
+                false, Map.of("dataSources", "orders (HikariCP)"));
+        AppFeatures merged = before.merge(after);
+        assertEquals("HikariCP", merged.dataSources().get(0).poolType(), "the 
pool type known after the pool started");
+        assertEquals("orders (HikariCP)", merged.signals().get("dataSources"));
+    }
+
+    @Test
+    void tracingOffersOnlyWhatIsOn() {
+        AppFeatures messageTracing = new AppFeatures(
+                List.of(), List.of(), false, false, List.of(), false, true, 
false, Map.of());
+        ToolGroups.Selection s = ToolGroups.select(messageTracing);
+        assertEquals(List.of("camel_runtime_trace"), s.mcpTools());
+        assertTrue(s.guidance().get(0).contains("message tracing is on"), 
s.guidance().get(0));
+    }
+
+    @Test
+    void theGuidanceNamesWhatTheIntegrationHas() {
+        List<String> guidance = ToolGroups.select(everything()).guidance();
+        assertEquals(3, guidance.size());
+        assertEquals("SQL: datasource(s) orders (HikariCP), audit, used by sql 
endpoints. Table names come from the"
+                     + " SQL trace (camel_runtime_sql_trace); don't guess a 
schema.",
+                guidance.get(0));
+        assertTrue(guidance.get(1).contains("OpenTelemetry"), guidance.get(1));
+        assertTrue(guidance.get(2).startsWith("Circuit breakers in routes pay, 
ship: camel_runtime_circuit_breakers"),
+                guidance.get(2));
+        for (String line : guidance) {
+            assertFalse(line.contains("\n"), "one line per group: " + line);
+        }
+    }
+
+    @Test
+    void theFingerprintIsStable() {
+        AppFeatures reordered = new AppFeatures(
+                List.of(new AppFeatures.DataSource("audit", null), new 
AppFeatures.DataSource("orders", "HikariCP")),
+                List.of("sql"), true, true, List.of("ship", "pay"), true, 
true, true, Map.of("x", "y"));
+        String fp = ToolGroups.select(everything()).fingerprint();
+        assertEquals("resilience,sql,tracing|audit,orders", fp);
+        assertEquals(fp, ToolGroups.select(reordered).fingerprint());
+        assertNotEquals(fp, 
ToolGroups.select(AppFeatures.none()).fingerprint());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
index 671a88db8d40..36100dbb7f73 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -23,6 +23,7 @@ import java.util.Set;
 
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -168,6 +169,38 @@ class ToolRegistryTest {
                 () -> ToolRegistry.execute("execute_sql", ctx, Map.of()));
     }
 
+    @Test
+    void theToolGroupsToolIsInternal() {
+        // the MCP servers export every camel_* tool by name; get_tool_groups 
is reached through
+        // camel_runtime_tool_groups only
+        ToolDescriptor tool = ToolRegistry.findTool("get_tool_groups");
+        assertNotNull(tool);
+        assertTrue(tool.isReadOnly());
+        assertFalse(tool.isDeterministic());
+        assertFalse(ToolRegistry.authoringTools().contains(tool));
+    }
+
+    @Test
+    void toolGroupsAnswerFromTheStatus() throws Exception {
+        ToolContext ctx = new ToolContext();
+        ctx.selectProcess(99999);
+        String json = "{'context': {'name': 'shop'}, 'dataSources': 
{'dataSources': [{'name': 'orders',"
+                      + " 'poolType': 'HikariCP'}]}}";
+        JsonObject status = (JsonObject) Jsoner.deserialize(json.replace('\'', 
'"'));
+        JsonObject answer = ToolRegistry.toolGroups(ctx, status);
+        assertEquals("shop", answer.get("app"));
+        assertEquals(99999L, answer.get("pid"));
+        assertEquals("sql|orders", answer.get("fingerprint"));
+        JsonArray groups = (JsonArray) answer.get("groups");
+        assertEquals(1, groups.size());
+        JsonObject sql = (JsonObject) groups.get(0);
+        assertEquals("sql", sql.get("id"));
+        assertTrue(((JsonArray) 
sql.get("tools")).contains("camel_runtime_sql"));
+        assertTrue(sql.get("guidance").toString().contains("orders 
(HikariCP)"));
+        assertTrue(((JsonArray) 
answer.get("core")).contains("camel_get_errors"));
+        assertEquals("orders", ((JsonObject) 
answer.get("signals")).get("dataSources"));
+    }
+
     @Test
     void circuitBreakerToolRequiresProcess() {
         ToolContext ctx = new ToolContext();
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 01ebc047c158..f8fbe7bdbd48 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -286,6 +286,17 @@ public class RuntimeTools {
         return delegateToRegistry("execute_sql", nameOrPid, args);
     }
 
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = """
+                  Which runtime tool groups the Camel application needs, from 
what it has: sql (datasources, \
+                  SQL endpoints), tracing (OpenTelemetry, message tracing, 
Micrometer) and resilience (circuit \
+                  breakers). Returns the core tools, each group's tools with 
one line of guidance, and a fingerprint \
+                  that changes only when the groups do. A client for a small 
model offers the core tools plus these.""")
+    public JsonObject camel_runtime_tool_groups(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid) {
+        return delegateToRegistry("get_tool_groups", nameOrPid, Map.of());
+    }
+
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the datasources of the running Camel application with 
their connection pool status: \
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
index 851060c78f88..ebd305f6313f 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
@@ -16,8 +16,10 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
+import java.lang.reflect.Method;
 import java.util.List;
 
+import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.junit.jupiter.api.Test;
@@ -75,10 +77,20 @@ class RuntimeToolsTest {
                 .hasMessageContaining("query is required");
     }
 
+    @Test
+    void theToolGroupsToolIsVisibleAtTheReadOnlyAccessLevel() throws Exception 
{
+        // McpAccessFilter decides from the annotations: a read-only hint 
keeps the tool for a read-only client
+        Method m = 
RuntimeTools.class.getDeclaredMethod("camel_runtime_tool_groups", String.class);
+        Tool tool = m.getAnnotation(Tool.class);
+        assertThat(McpSecurityConfig.AccessLevel.READ_ONLY.permits(
+                tool.annotations().readOnlyHint(), 
tool.annotations().destructiveHint())).isTrue();
+    }
+
     @Test
     void theNewWrappersDelegateToRegistryTools() {
         // CAMEL-24867: every wrapper names a tool the shared registry has, so 
a typo cannot hide until runtime
-        for (String name : List.of("execute_sql", "get_datasources", 
"get_sql_trace", "get_circuit_breakers", "get_metrics",
+        for (String name : List.of("execute_sql", "get_tool_groups", 
"get_datasources", "get_sql_trace",
+                "get_circuit_breakers", "get_metrics",
                 "get_eip_stats", "get_spans", "get_startup_steps", 
"get_route_analysis", "detect_config_drift")) {
             assertThat(ToolRegistry.findTool(name)).as(name).isNotNull();
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 78e0e8f7b457..b2e967781909 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -33,6 +33,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -75,6 +76,7 @@ import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 import org.apache.camel.dsl.jbang.core.commands.LlmClient;
 import org.apache.camel.dsl.jbang.core.commands.ai.AnswerChecks;
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
 import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.util.json.JsonObject;
@@ -301,12 +303,31 @@ class AiPanel {
         AcpAgentClient create(AiProviderSelector.AcpPreset preset, Path cwd) 
throws IOException;
     }
 
+    /** The selected integration as the tool groups see it; replaced in tests. 
*/
+    interface AppStatusSource {
+        /** The selected pid, null when none. */
+        String selectedPid();
+
+        /** How often it reloaded its routes. */
+        int reloadCount();
+
+        /** What it has, read from its status. */
+        AppFeatures features();
+    }
+
     // MCP facade for TUI tool access from the AI panel
     private McpFacade mcpFacade;
     // /write: confirm (dialog per write), auto (the model may skip it with 
confirm=false) or live (the edit is replayed
     // in the source editor and the user saves or discards it)
     private McpFacade.WriteMode writeMode = McpFacade.WriteMode.CONFIRM;
     private TuiToolRegistry toolRegistry;
+    // CAMEL-24834: the tool groups (SQL, tracing, resilience) of the selected 
integration that the core set gets, see
+    // refreshToolGroups()
+    private volatile AppStatusSource appStatusSource;
+    private volatile String toolGroupsPid;
+    private volatile int toolGroupsReloads = -1;
+    private volatile AppFeatures toolGroupsFeatures = AppFeatures.none();
+    private volatile TuiToolGroups.Selection toolGroups = 
TuiToolGroups.Selection.none();
     private boolean mcpServerActive;
     private int mcpServerPort;
 
@@ -1561,6 +1582,7 @@ class AiPanel {
         if (!testingClientInjected) {
             toolMode = normalizeToolMode(TuiSettings.load().getAiTools());
         }
+        refreshToolGroups();
         tools = buildTuiToolDefinitions();
         String systemPrompt = buildSystemPrompt();
 
@@ -3328,9 +3350,63 @@ class AiPanel {
             sb.append("\nThe TUI MCP server is available at http://localhost:";)
                     .append(mcpServerPort).append("/mcp for external AI 
agents.");
         }
+        // last, so the prefix above stays cached when the groups change
+        if (useCoreTools() && !toolGroups.guidance().isEmpty()) {
+            sb.append(mcpServerActive ? "\n" : "").append("\nThe selected 
integration:\n");
+            toolGroups.guidance().forEach(line -> sb.append("- 
").append(line).append('\n'));
+        }
         return sb.toString();
     }
 
+    /**
+     * Loads the tool groups of the selected integration for the core set 
(CAMEL-24834): the SQL tools when it has a
+     * database, the guidance for its tracing and circuit breakers. Read again 
only when another integration is selected
+     * or the selected one reloaded (the groups then only grow: what it had 
before still counts), so the tools and the
+     * prompt stay the same from question to question and a local model's 
prompt cache keeps working. While an
+     * integration has no groups yet its status is read again, since one that 
just started may not have written it
+     * completely. The full set is not affected.
+     */
+    private void refreshToolGroups() {
+        if (!useCoreTools()) {
+            return;
+        }
+        AppStatusSource source = appStatusSource != null ? appStatusSource : 
facadeStatusSource();
+        String pid = source != null ? source.selectedPid() : null;
+        int reloads = source != null ? source.reloadCount() : 0;
+        boolean samePid = Objects.equals(pid, toolGroupsPid);
+        if (samePid && reloads == toolGroupsReloads && 
!toolGroups.groups().isEmpty()) {
+            return;
+        }
+        AppFeatures read = pid != null ? source.features() : 
AppFeatures.none();
+        toolGroupsFeatures = samePid ? toolGroupsFeatures.merge(read) : read;
+        toolGroupsPid = pid;
+        toolGroupsReloads = reloads;
+        toolGroups = TuiToolGroups.select(toolGroupsFeatures);
+    }
+
+    private AppStatusSource facadeStatusSource() {
+        McpFacade facade = mcpFacade;
+        if (facade == null) {
+            return null;
+        }
+        return new AppStatusSource() {
+            @Override
+            public String selectedPid() {
+                return facade.getSelectedPid();
+            }
+
+            @Override
+            public int reloadCount() {
+                return facade.getSelectedReloadCount();
+            }
+
+            @Override
+            public AppFeatures features() {
+                return AppFeatures.fromStatus(facade.readSelectedStatus());
+            }
+        };
+    }
+
     /**
      * Prefixes the question with the integration the user is looking at. This 
used to live in the system prompt, but
      * there it invalidated the model's cached prompt prefix every time the 
selection changed.
@@ -3368,11 +3444,18 @@ class AiPanel {
             return "no tools available";
         }
         int total = toolRegistry.getToolDefinitions().size();
-        int active = useCoreTools() ? 
toolRegistry.getCoreToolDefinitions().size() : total;
+        int active = useCoreTools() ? 
toolRegistry.getCoreToolDefinitions(toolGroups.tools()).size() : total;
         String mode = toolMode == null ? TOOL_MODE_AUTO : toolMode;
         String detail = TOOL_MODE_AUTO.equals(mode)
                 ? (useCoreTools() ? " (local provider)" : " (hosted 
provider)") : "";
-        return (useCoreTools() ? "core" : "full") + " (" + active + " of " + 
total + " tools), mode " + mode + detail;
+        String groups = "";
+        if (useCoreTools()) {
+            groups = toolGroups.groups().isEmpty()
+                    ? "; groups: none loaded"
+                    : "; groups: " + toolGroups.groupIds() + " (from the 
selected integration)";
+        }
+        return (useCoreTools() ? "core" : "full") + " (" + active + " of " + 
total + " tools), mode " + mode + detail
+               + groups;
     }
 
     private List<LlmClient.ToolDef> buildTuiToolDefinitions() {
@@ -3380,8 +3463,8 @@ class AiPanel {
             return List.of();
         }
         List<LlmClient.ToolDef> defs = new ArrayList<>();
-        List<TuiToolRegistry.ToolDef> source
-                = useCoreTools() ? toolRegistry.getCoreToolDefinitions() : 
toolRegistry.getToolDefinitions();
+        List<TuiToolRegistry.ToolDef> source = useCoreTools()
+                ? toolRegistry.getCoreToolDefinitions(toolGroups.tools()) : 
toolRegistry.getToolDefinitions();
         for (TuiToolRegistry.ToolDef td : source) {
             defs.add(new LlmClient.ToolDef(td.name(), td.description(), 
td.inputSchema()));
         }
@@ -3975,6 +4058,18 @@ class AiPanel {
         return buildSystemPrompt();
     }
 
+    void setAppStatusSourceForTesting(AppStatusSource source) {
+        this.appStatusSource = source;
+    }
+
+    void refreshToolGroupsForTesting() {
+        refreshToolGroups();
+    }
+
+    TuiToolGroups.Selection toolGroupsForTesting() {
+        return toolGroups;
+    }
+
     List<LlmClient.ToolDef> toolDefinitionsForTesting() {
         return buildTuiToolDefinitions();
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index f6915ee3923d..23d12664c4f7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -344,6 +344,28 @@ class McpFacade {
         return info != null ? info.name : null;
     }
 
+    /** How often the selected integration has reloaded its routes, 0 when 
nothing is selected. */
+    int getSelectedReloadCount() {
+        if (ctx == null) {
+            return 0;
+        }
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.reloaded : 0;
+    }
+
+    /** The status document of the selected integration, null when nothing is 
selected or it has none yet. */
+    JsonObject readSelectedStatus() {
+        if (ctx == null || ctx.selectedPid == null) {
+            return null;
+        }
+        try {
+            return RuntimeHelper.readStatus(Long.parseLong(ctx.selectedPid));
+        } catch (RuntimeException e) {
+            // not a pid, or a status that cannot be read: no tool groups, the 
core set is still offered
+            return null;
+        }
+    }
+
     String getSelectedCamelVersion() {
         if (ctx == null) {
             return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
new file mode 100644
index 000000000000..448841972cff
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroup;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroups;
+
+/**
+ * The AI panel's side of {@link ToolGroups} (CAMEL-24834): the tui_* tools 
each group adds to the core set, and the
+ * guidance line in the panel's words. Only SQL needs tools of its own; the 
spans, the traced messages, the metrics and
+ * the circuit breakers are read with core tools (tui_get_spans, 
tui_get_history, tui_get_table), so those groups only
+ * add the line that tells a small model where to look.
+ */
+final class TuiToolGroups {
+
+    static final String SQL_TOOL = "tui_execute_sql";
+    static final String UPDATE_ROW_TOOL = "tui_update_row";
+
+    /**
+     * The groups of an integration as the panel uses them.
+     *
+     * @param groups   the loaded groups
+     * @param tools    the tools the groups add to the core set
+     * @param guidance one line per group, appended to the system prompt
+     */
+    record Selection(List<ToolGroup> groups, List<String> tools, List<String> 
guidance) {
+
+        Selection {
+            groups = List.copyOf(groups);
+            tools = List.copyOf(tools);
+            guidance = List.copyOf(guidance);
+        }
+
+        static Selection none() {
+            return new Selection(List.of(), List.of(), List.of());
+        }
+
+        String groupIds() {
+            return 
groups.stream().map(ToolGroup::id).collect(Collectors.joining(", "));
+        }
+    }
+
+    private TuiToolGroups() {
+    }
+
+    static Selection select(AppFeatures features) {
+        AppFeatures f = features != null ? features : AppFeatures.none();
+        List<ToolGroup> groups = ToolGroups.groups(f);
+        List<String> tools = new ArrayList<>();
+        List<String> guidance = new ArrayList<>();
+        for (ToolGroup group : groups) {
+            tools.addAll(tools(group));
+            guidance.add(guidance(group, f));
+        }
+        return new Selection(groups, tools, guidance);
+    }
+
+    static List<String> tools(ToolGroup group) {
+        if (group == ToolGroup.SQL) {
+            return List.of(SQL_TOOL, UPDATE_ROW_TOOL);
+        }
+        return List.of();
+    }
+
+    static String guidance(ToolGroup group, AppFeatures f) {
+        return switch (group) {
+            case SQL -> "SQL: " + ToolGroups.describeSql(f)
+                        + ". Table names come from the SQL trace 
(tui_get_table tab 'SQL Trace'); don't guess a schema.";
+            case TRACING -> {
+                List<String> parts = new ArrayList<>();
+                if (f.openTelemetry()) {
+                    parts.add("OpenTelemetry is on, tui_get_spans has the 
spans per trace");
+                }
+                if (f.messageTracing()) {
+                    parts.add("message tracing is on, tui_get_history has the 
traced messages");
+                }
+                if (f.micrometer()) {
+                    parts.add("tui_get_table tab 'Metrics' has the Micrometer 
metrics");
+                }
+                yield "Tracing: " + String.join("; ", parts) + ".";
+            }
+            case RESILIENCE -> ToolGroups.describeBreakers(f)
+                               + ": tui_get_table tab 'Circuit Breaker' shows 
state (CLOSED/OPEN/HALF_OPEN) and failure"
+                               + " rate; OPEN means the fallback runs.";
+        };
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 2fbe310fa854..89b2af9b9e4a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -264,6 +265,16 @@ class TuiToolRegistry {
         return getToolDefinitions().stream().filter(t -> 
CORE_TOOLS.contains(t.name())).toList();
     }
 
+    /**
+     * Returns the {@link #CORE_TOOLS} definitions plus the given ones (the 
tools of the integration's tool groups, see
+     * {@link TuiToolGroups}), in registry order.
+     */
+    List<ToolDef> getCoreToolDefinitions(Collection<String> extra) {
+        return getToolDefinitions().stream()
+                .filter(t -> CORE_TOOLS.contains(t.name()) || 
extra.contains(t.name()))
+                .toList();
+    }
+
     /**
      * Executes a tool by name, returns result string.
      */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
index 39fa18662189..fca8515e5783 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
@@ -17,6 +17,10 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.LlmClient;
 import org.apache.camel.util.json.JsonObject;
@@ -55,6 +59,11 @@ class AiPanelPromptBudgetTest {
     // raised from 9450 for camel_project_overview and 
camel_save_project_summary (CAMEL-25143), measured ~9750:
     // full mode only (hosted models), and the panel's own /overview sends no 
tools at all
     static final int FULL_BUDGET_TOKENS = 9_850;
+    /** Measured ~5.2k tokens for 26 tools: the core set (~4.7k) plus every 
tool group (CAMEL-24834). */
+    // the SQL group adds tui_execute_sql and tui_update_row (~385 tokens), 
each group one guidance line in the prompt
+    // (~130 for all three); an integration rarely has all three, and the 
groups only load for the integration that
+    // needs them
+    static final int CORE_WITH_GROUPS_BUDGET_TOKENS = 5_500;
 
     record Prefix(String mode, int tools, long promptChars, long toolChars) {
 
@@ -99,10 +108,23 @@ class AiPanelPromptBudgetTest {
         AiPanel panel = new AiPanel();
         panel.setToolRegistryForTesting(new TuiToolRegistry(null));
         panel.setToolModeForTesting(mode);
+        return measure(mode, panel);
+    }
+
+    static Prefix measure(String mode, AiPanel panel) {
         List<LlmClient.ToolDef> defs = panel.toolDefinitionsForTesting();
         return new Prefix(mode, defs.size(), 
panel.systemPromptForTesting().length(), wireChars(defs));
     }
 
+    /** A core panel with every tool group loaded: datasources, OpenTelemetry, 
tracing, Micrometer, circuit breakers. */
+    static AiPanel coreWithAllGroups() {
+        AiPanelToolGroupsTest.FakeApp app = new 
AiPanelToolGroupsTest.FakeApp();
+        app.features = AiPanelToolGroupsTest.EVERYTHING;
+        AiPanel panel = AiPanelToolGroupsTest.panel(AiPanel.TOOL_MODE_CORE, 
app);
+        panel.refreshToolGroupsForTesting();
+        return panel;
+    }
+
     @Test
     void corePrefixStaysWithinBudget() {
         Prefix core = measure(AiPanel.TOOL_MODE_CORE);
@@ -112,6 +134,17 @@ class AiPanelPromptBudgetTest {
                 "core prefix grew to ~" + core.totalTokens() + " tokens, 
budget " + CORE_BUDGET_TOKENS + ": " + core);
     }
 
+    @Test
+    void coreWithAllGroupsPrefixStaysWithinBudget() {
+        Prefix groups = measure("core+groups", coreWithAllGroups());
+        System.out.println("AI panel static prefix: " + groups);
+
+        assertTrue(groups.totalTokens() <= CORE_WITH_GROUPS_BUDGET_TOKENS,
+                "core prefix with all groups grew to ~" + groups.totalTokens() 
+ " tokens, budget "
+                                                                           + 
CORE_WITH_GROUPS_BUDGET_TOKENS + ": "
+                                                                           + 
groups);
+    }
+
     @Test
     void fullPrefixStaysWithinBudget() {
         Prefix full = measure(AiPanel.TOOL_MODE_FULL);
@@ -154,5 +187,20 @@ class AiPanelPromptBudgetTest {
         for (String prompt : List.of(core, full)) {
             assertTrue(prompt.contains("camel_write_file"), "write files with 
the tool");
         }
+
+        // CAMEL-24834: the guidance of the tool groups only names tools the 
model is given
+        AiPanel groups = coreWithAllGroups();
+        String prompt = groups.systemPromptForTesting();
+        int start = prompt.indexOf("The selected integration:");
+        assertTrue(start > 0, "the guidance is appended at the end");
+        Set<String> tools = 
groups.toolDefinitionsForTesting().stream().map(LlmClient.ToolDef::name)
+                .collect(Collectors.toSet());
+        Matcher m = 
Pattern.compile("\\b(?:tui|camel)_[a-z_]+").matcher(prompt.substring(start));
+        int named = 0;
+        while (m.find()) {
+            named++;
+            assertTrue(tools.contains(m.group()), m.group() + " is named in 
the guidance but not in the set");
+        }
+        assertTrue(named >= 4, "the guidance names the tools to use");
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
new file mode 100644
index 000000000000..53fb1bf07691
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroup;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-24834: the AI panel loads the tool groups of the selected integration 
into the core set, and reads them again
+ * only when the integration or its routes change.
+ */
+class AiPanelToolGroupsTest {
+
+    static final AppFeatures SQL = new AppFeatures(
+            List.of(new AppFeatures.DataSource("orders", "HikariCP")), 
List.of("sql"), true, false, List.of(),
+            false, false, false, Map.of());
+    static final AppFeatures BREAKERS = new AppFeatures(
+            List.of(), List.of(), false, true, List.of("pay"), false, false, 
false, Map.of());
+    static final AppFeatures OTEL = new AppFeatures(
+            List.of(), List.of(), false, false, List.of(), true, false, false, 
Map.of());
+    static final AppFeatures EVERYTHING = SQL.merge(BREAKERS).merge(new 
AppFeatures(
+            List.of(), List.of(), false, false, List.of(), true, true, true, 
Map.of()));
+
+    /** A selected integration whose pid, reload count and features a test 
changes, counting the status reads. */
+    static final class FakeApp implements AiPanel.AppStatusSource {
+        String pid = "100";
+        int reloads;
+        AppFeatures features = SQL;
+        int reads;
+
+        @Override
+        public String selectedPid() {
+            return pid;
+        }
+
+        @Override
+        public int reloadCount() {
+            return reloads;
+        }
+
+        @Override
+        public AppFeatures features() {
+            reads++;
+            return features;
+        }
+    }
+
+    static AiPanel panel(String mode, FakeApp app) {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        panel.setToolModeForTesting(mode);
+        panel.setAppStatusSourceForTesting(app);
+        return panel;
+    }
+
+    static List<String> toolNames(AiPanel panel) {
+        return 
panel.toolDefinitionsForTesting().stream().map(LlmClient.ToolDef::name).toList();
+    }
+
+    @Test
+    void theSqlGroupAddsTheSqlTools() {
+        AiPanel panel = panel(AiPanel.TOOL_MODE_CORE, new FakeApp());
+        assertFalse(toolNames(panel).contains("tui_execute_sql"), "no group 
before the first question");
+
+        panel.refreshToolGroupsForTesting();
+        assertEquals(List.of(ToolGroup.SQL), 
panel.toolGroupsForTesting().groups());
+        assertTrue(toolNames(panel).containsAll(List.of("tui_execute_sql", 
"tui_update_row")));
+        assertTrue(panel.systemPromptForTesting().contains(
+                "- SQL: datasource(s) orders (HikariCP), used by sql 
endpoints. Table names come from the SQL trace"
+                                                           + " (tui_get_table 
tab 'SQL Trace'); don't guess a schema.\n"));
+        assertTrue(panel.describeToolModeForTesting().contains("groups: sql 
(from the selected integration)"),
+                panel.describeToolModeForTesting());
+    }
+
+    @Test
+    void anotherIntegrationGetsItsOwnGroups() {
+        FakeApp app = new FakeApp();
+        AiPanel panel = panel(AiPanel.TOOL_MODE_CORE, app);
+        panel.refreshToolGroupsForTesting();
+        assertEquals(List.of(ToolGroup.SQL), 
panel.toolGroupsForTesting().groups());
+
+        app.pid = "200";
+        app.features = OTEL;
+        panel.refreshToolGroupsForTesting();
+        assertEquals(2, app.reads);
+        assertEquals(List.of(ToolGroup.TRACING), 
panel.toolGroupsForTesting().groups(), "no union across integrations");
+        assertFalse(toolNames(panel).contains("tui_execute_sql"));
+        assertTrue(panel.systemPromptForTesting().contains("tui_get_spans has 
the spans"));
+    }
+
+    @Test
+    void aReloadAddsToTheGroups() {
+        FakeApp app = new FakeApp();
+        AiPanel panel = panel(AiPanel.TOOL_MODE_CORE, app);
+        panel.refreshToolGroupsForTesting();
+
+        app.reloads = 1;
+        app.features = BREAKERS;
+        panel.refreshToolGroupsForTesting();
+        assertEquals(2, app.reads);
+        assertEquals(List.of(ToolGroup.SQL, ToolGroup.RESILIENCE), 
panel.toolGroupsForTesting().groups(),
+                "what it had before the reload still counts");
+        assertTrue(panel.systemPromptForTesting().contains("Circuit breakers 
in routes pay: tui_get_table tab"));
+    }
+
+    @Test
+    void theGroupsAreCachedOtherwise() {
+        FakeApp app = new FakeApp();
+        AiPanel panel = panel(AiPanel.TOOL_MODE_CORE, app);
+        panel.refreshToolGroupsForTesting();
+        String prompt = panel.systemPromptForTesting();
+        List<LlmClient.ToolDef> tools = panel.toolDefinitionsForTesting();
+
+        app.features = EVERYTHING;
+        for (int i = 0; i < 3; i++) {
+            panel.refreshToolGroupsForTesting();
+        }
+        assertEquals(1, app.reads, "same integration, no reload: the status is 
not read again");
+        assertEquals(prompt, panel.systemPromptForTesting(), "the prompt stays 
byte-identical");
+        assertEquals(tools, panel.toolDefinitionsForTesting());
+    }
+
+    @Test
+    void anIntegrationWithoutGroupsIsReadAgain() {
+        // one that just started may not have written its status completely yet
+        FakeApp app = new FakeApp();
+        app.features = AppFeatures.none();
+        AiPanel panel = panel(AiPanel.TOOL_MODE_CORE, app);
+        panel.refreshToolGroupsForTesting();
+        assertTrue(panel.toolGroupsForTesting().groups().isEmpty());
+        assertTrue(panel.describeToolModeForTesting().contains("groups: none 
loaded"));
+
+        app.features = SQL;
+        panel.refreshToolGroupsForTesting();
+        assertEquals(List.of(ToolGroup.SQL), 
panel.toolGroupsForTesting().groups());
+    }
+
+    @Test
+    void theFullSetIsUnchanged() {
+        FakeApp app = new FakeApp();
+        app.features = EVERYTHING;
+        AiPanel plain = panel(AiPanel.TOOL_MODE_FULL, new FakeApp());
+        AiPanel panel = panel(AiPanel.TOOL_MODE_FULL, app);
+        String before = panel.systemPromptForTesting();
+        panel.refreshToolGroupsForTesting();
+
+        assertEquals(0, app.reads, "the full set does not need the status");
+        assertEquals(before, panel.systemPromptForTesting());
+        assertFalse(panel.systemPromptForTesting().contains("The selected 
integration"));
+        assertEquals(toolNames(plain), toolNames(panel));
+        assertTrue(toolNames(panel).contains("tui_update_row"));
+    }
+}

Reply via email to