Croway commented on code in PR #24473:
URL: https://github.com/apache/camel/pull/24473#discussion_r3543981379


##########
components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc:
##########
@@ -0,0 +1,241 @@
+= AI Tool Component
+:doctitle: AI Tool
+:shortname: ai-tool
+:artifactid: camel-ai-tool
+:description: Framework-agnostic consumer endpoint that registers a Camel 
route as an LLM tool in the shared AiToolRegistry.
+:since: 4.22
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only consumer is supported
+//Manually maintained attributes
+:group: AI
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The AI Tool component provides a framework-agnostic way to expose Camel routes 
as tools that AI models can call.
+Tools registered via this component are stored in a shared `AiToolRegistry` 
and can be discovered by any AI producer
+component (such as xref:langchain4j-agent-component.adoc[LangChain4j Agent] or 
xref:spring-ai-chat-component.adoc[Spring AI Chat])
+using tag-based filtering.
+
+Maven users will need to add the following dependency to their `pom.xml`
+for this component:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>camel-ai-tool</artifactId>
+    <version>x.x.x</version>
+    <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI format
+
+----
+ai-tool:toolName[?options]
+----
+
+Where *toolName* is the name the LLM sees and uses to invoke the tool.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+Define a tool by creating a consumer route with the `ai-tool:` scheme.
+The route body processes tool invocations and returns results to the AI model.
+
+=== Basic Tool Definition
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("ai-tool:weather?tags=weather&description=Get current weather for a city" 
+
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:weather?tags=weather&amp;description=Get current weather 
for a city&amp;parameter.city=string&amp;parameter.city.description=The city 
name"/>
+  <to uri="bean:weatherService"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: ai-tool:weather
+      parameters:
+        tags: weather
+        description: "Get current weather for a city"
+        parameter.city: string
+        parameter.city.description: "The city name"
+      steps:
+        - to:
+            uri: bean:weatherService
+----
+====
+
+=== Tool with Multiple Parameters
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("ai-tool:calculator?tags=math" +
+    "&description=Calculate a math expression" +
+    "&parameter.a=number" +
+    "&parameter.a.description=First operand" +
+    "&parameter.a.required=true" +
+    "&parameter.b=number" +
+    "&parameter.b.description=Second operand" +
+    "&parameter.b.required=true" +
+    "&parameter.operation=string" +
+    "&parameter.operation.description=The operation to perform" +
+    "&parameter.operation.enum=add,subtract,multiply,divide")
+    .to("direct:calculator");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:calculator?tags=math&amp;description=Calculate a math 
expression&amp;parameter.a=number&amp;parameter.a.description=First 
operand&amp;parameter.a.required=true&amp;parameter.b=number&amp;parameter.b.description=Second
 
operand&amp;parameter.b.required=true&amp;parameter.operation=string&amp;parameter.operation.description=The
 operation to 
perform&amp;parameter.operation.enum=add,subtract,multiply,divide"/>
+  <to uri="direct:calculator"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: ai-tool:calculator
+      parameters:
+        tags: math
+        description: "Calculate a math expression"
+        parameter.a: number
+        parameter.a.description: "First operand"
+        parameter.a.required: true
+        parameter.b: number
+        parameter.b.description: "Second operand"
+        parameter.b.required: true
+        parameter.operation: string
+        parameter.operation.description: "The operation to perform"
+        parameter.operation.enum: "add,subtract,multiply,divide"
+      steps:
+        - to:
+            uri: direct:calculator
+----
+====
+
+=== Tag-Based Discovery
+
+Tags group tools so that AI producers can select relevant subsets:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// Define tools with tags
+from("ai-tool:weather?tags=weather,external-api&description=Get weather for a 
city" +
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+
+from("ai-tool:queryUser?tags=users&description=Query database by user ID" +
+    "&parameter.id=integer&parameter.id.description=The user 
ID&parameter.id.required=true")
+    .to("sql:SELECT name FROM users WHERE id = :#id");

Review Comment:
   This example won't work: `:#id` binds from headers/body, but tool arguments 
are in the `AiToolArguments` exchange variable. Either use 
`:#${variable.AiToolArguments.parameters[id]}` or add a processor step that 
extracts `args.getInteger("id")` into a header first. Worth fixing since it's 
the pattern users will copy (the XML and YAML tabs have the same issue).



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolExecutor.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Framework-agnostic executor for Camel route tools. Handles the common logic 
of resolving the route processor from the
+ * tool's consumer, populating an {@link Exchange} with tool arguments, 
invoking the route, and returning the result.
+ * <p>
+ * AI framework adapters (LangChain4j, Spring AI, OpenAI) only need to parse 
their native argument format into a
+ * {@code Map<String, Object>} and call this executor — they do not need to 
know how routes are resolved or invoked.
+ * <p>
+ * Returns an {@link AiToolResult} that classifies the outcome without 
deciding error handling policy. Framework
+ * adapters inspect the result type and decide whether to return the error 
message as a string to the LLM, rethrow the
+ * cause so framework-level error handlers fire, or sanitize the message 
before returning it.
+ * <p>
+ * This is an internal support class used by Camel AI framework adapters and 
is not intended for direct use by end
+ * users.
+ *
+ * @since 4.22
+ */
+public final class AiToolExecutor {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiToolExecutor.class);
+
+    private AiToolExecutor() {
+    }
+
+    /**
+     * Executes a Camel route tool by resolving the route processor from the 
spec's consumer, populating the exchange
+     * with the provided arguments, and invoking the route.
+     * <p>
+     * Arguments are validated against the tool's declared parameters 
(undeclared arguments are warned about but not
+     * rejected, required arguments are checked) and then wrapped in an {@link 
AiToolArguments} object set as an
+     * exchange variable under {@link AiTool#TOOL_ARGUMENTS}. This avoids 
polluting the exchange header namespace and
+     * eliminates any risk of colliding with internal Camel headers.
+     * <p>
+     * The calling adapter owns the exchange lifecycle: it must create the 
exchange before calling this method and
+     * release it afterwards (via {@code consumer.releaseExchange()}) in a 
try-finally block.
+     * <p>
+     * All errors — validation failures and route execution errors — are 
caught and returned as typed
+     * {@link AiToolResult} variants rather than propagated. Framework 
adapters inspect the result type and decide how
+     * to handle errors (return to LLM, rethrow, sanitize).
+     *
+     * @param  spec      the tool specification containing the consumer and 
declared parameters
+     * @param  arguments the tool arguments as a name-value map; each 
framework adapter is responsible for parsing its
+     *                   native format (JSON string, Map, etc.) into this map 
before calling
+     * @param  exchange  the Camel exchange to populate with arguments and 
execute
+     * @return           an {@link AiToolResult} classifying the outcome; 
never null
+     */
+    public static AiToolResult execute(AiToolSpec spec, Map<String, Object> 
arguments, Exchange exchange) {
+        String toolName = spec.getName();
+
+        DefaultConsumer consumer = spec.getConsumer();
+        if (consumer == null) {
+            IllegalStateException cause = new IllegalStateException(
+                    String.format("No consumer available for tool '%s'", 
toolName));
+            return new AiToolResult.ExecutionError(cause.getMessage(), cause);
+        }
+
+        Processor routeProcessor = consumer.getProcessor();
+        if (routeProcessor == null) {
+            IllegalStateException cause = new IllegalStateException(
+                    String.format("No route processor available for tool 
'%s'", toolName));
+            return new AiToolResult.ExecutionError(cause.getMessage(), cause);
+        }
+
+        LOG.debug("Executing Camel route tool: '{}'", toolName);
+
+        // Warn about undeclared arguments but do not reject them -- LLMs 
frequently
+        // hallucinate extra parameters and rejecting them would break valid 
tool calls.
+        if (arguments != null && !arguments.isEmpty() && 
!spec.getParameterDefs().isEmpty()) {
+            Set<String> declaredParams = spec.getParameterDefs().keySet();
+
+            for (String name : arguments.keySet()) {
+                if (!declaredParams.contains(name)) {
+                    LOG.warn("Undeclared tool argument '{}' for tool '{}' -- 
the LLM sent a parameter "
+                             + "that is not declared in the tool 
specification; ignoring it",
+                            name, toolName);
+                }
+            }
+        }
+
+        for (Map.Entry<String, AiToolParameterHelper.ParameterDef> entry : 
spec.getParameterDefs().entrySet()) {
+            if (entry.getValue().isRequired()
+                    && (arguments == null || 
!arguments.containsKey(entry.getKey()))) {
+                LOG.warn("Missing required argument '{}' for tool '{}' -- the 
LLM did not send "
+                         + "a parameter that is declared as required in the 
tool specification",
+                        entry.getKey(), toolName);
+                IllegalArgumentException cause = new IllegalArgumentException(
+                        String.format("Missing required argument '%s' for tool 
'%s'", entry.getKey(), toolName));
+                return new AiToolResult.ArgumentError(cause.getMessage(), 
cause);
+            }
+        }
+
+        // Defensive copy so callers cannot mutate arguments during route 
execution
+        Map<String, Object> argsCopy = arguments != null ? new 
HashMap<>(arguments) : Map.of();
+        exchange.setVariable(AiTool.TOOL_ARGUMENTS, new 
AiToolArguments(toolName, argsCopy));
+
+        // Execute the route
+        try {
+            routeProcessor.process(exchange);
+        } catch (Exception e) {
+            LOG.error("Error executing tool '{}': {}", toolName, 
e.getMessage(), e);
+            return new AiToolResult.ExecutionError(
+                    String.format("Error executing tool '%s': %s", toolName, 
e.getMessage()), e);
+        }
+
+        if (exchange.getException() != null) {
+            Exception routeError = exchange.getException();
+            LOG.error("Error executing tool '{}': {}", toolName, 
routeError.getMessage(), routeError);
+            return new AiToolResult.ExecutionError(
+                    String.format("Error executing tool '%s': %s", toolName, 
routeError.getMessage()), routeError);
+        }
+
+        String result = exchange.getMessage().getBody(String.class);
+        LOG.debug("Tool '{}' execution completed successfully", toolName);

Review Comment:
   This sits outside the try/catch, so a `TypeConversionException` (route 
returns a non-convertible body) propagates to the adapter — contradicting the 
documented contract that all errors come back as typed `AiToolResult` variants. 
Suggest wrapping it and returning an `ExecutionError`.



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolConsumer.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.Map;
+
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Consumer that registers a Camel route as an AI tool in the {@link 
AiToolRegistry} on start and deregisters on stop.
+ *
+ * @since 4.22
+ */
+public class AiToolConsumer extends DefaultConsumer {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiToolConsumer.class);
+
+    private final String toolName;
+    private final AiToolConfiguration configuration;
+    private AiToolSpec registeredSpec;
+    private String[] registeredTags;
+    private boolean registeredInDefaultPool;
+
+    public AiToolConsumer(AiToolEndpoint endpoint, Processor processor) {
+        super(endpoint, processor);
+        this.toolName = endpoint.getToolName();
+        this.configuration = endpoint.getConfiguration();
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        Map<String, String> params = configuration.getParameters();
+        Map<String, AiToolParameterHelper.ParameterDef> parameterDefs
+                = (params != null && !params.isEmpty())
+                        ? AiToolParameterHelper.parseParameterMetadata(params)
+                        : Map.of();
+
+        String jsonSchema = !parameterDefs.isEmpty()
+                ? AiToolParameterHelper.buildJsonSchemaFromDefs(parameterDefs)
+                : null;
+
+        registeredSpec = new AiToolSpec(
+                toolName, configuration.getDescription(), parameterDefs, 
jsonSchema, this);
+
+        AiToolRegistry registry = 
AiToolRegistry.getOrCreate(getEndpoint().getCamelContext());
+        String tags = configuration.getTags();
+        if (tags != null && !tags.isBlank()) {
+            registeredTags = AiToolParameterHelper.splitTags(tags);
+            registeredInDefaultPool = false;
+            for (String tag : registeredTags) {
+                LOG.debug("Registering tool '{}' with tag '{}'", toolName, 
tag);
+                registry.put(tag, registeredSpec);
+            }
+        } else {
+            registeredTags = null;
+            registeredInDefaultPool = true;
+            LOG.debug("Registering tool '{}' in default pool (no tags)", 
toolName);
+            registry.putDefault(registeredSpec);
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {

Review Comment:
   Route **suspend** doesn't deregister the tool (no `doSuspend` override), so 
a suspended route stays in the registry and the executor will still dispatch 
into its processor. Either deregister on suspend or have `AiToolExecutor` check 
the consumer's state before dispatching — and document whichever is chosen.



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolConfiguration.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+
+/**
+ * Configuration for the {@link AiToolComponent}: tool description, tags, and 
parameter metadata.
+ *
+ * @since 4.22
+ */
+@Configurer
+@UriParams
+public class AiToolConfiguration implements Cloneable {
+
+    @UriParam(description = "Comma-separated list of tags used to group tools. 
"
+                            + "Producers filter the registry by these tags to 
select which tools to expose to the LLM. "
+                            + "When omitted, the tool goes into a default pool 
available to all producers.")
+    private String tags;

Review Comment:
   Missing `@Metadata(label = "consumer")` — the generated catalog JSON shows 
`"label": ""` for `tags` while `description`/`parameters` have `"consumer"`.



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolRegistry.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * CamelContext-scoped registry mapping tags to {@link AiToolSpec} instances. 
AI components (LangChain4j, Spring AI)
+ * read from this registry to discover tools registered via the {@code 
ai-tool} consumer endpoint.
+ * <p>
+ * Each {@link CamelContext} gets its own registry instance, registered as a 
context plugin. Use
+ * {@link #getOrCreate(CamelContext)} to obtain the instance for a given 
context.
+ * <p>
+ * Replaces the duplicated {@code CamelToolExecutorCache} singletons from 
{@code camel-langchain4j-tools} and
+ * {@code camel-spring-ai-tools}.
+ *
+ * @since 4.22
+ */
+public final class AiToolRegistry {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AiToolRegistry.class);
+
+    private final Map<String, Set<AiToolSpec>> tools;
+    private final Set<AiToolSpec> defaultTools;
+
+    AiToolRegistry() {
+        tools = new ConcurrentHashMap<>();
+        defaultTools = Collections.synchronizedSet(new LinkedHashSet<>());
+    }
+
+    /**
+     * Returns the {@link AiToolRegistry} for the given {@link CamelContext}, 
creating and registering one as a context
+     * plugin if it does not yet exist.
+     */
+    public static AiToolRegistry getOrCreate(CamelContext context) {
+        synchronized (context) {
+            AiToolRegistry registry = context.getCamelContextExtension()
+                    .getContextPlugin(AiToolRegistry.class);
+            if (registry == null) {
+                registry = new AiToolRegistry();
+                context.getCamelContextExtension()
+                        .addContextPlugin(AiToolRegistry.class, registry);
+            }
+            return registry;
+        }
+    }
+
+    public void put(String tag, AiToolSpec spec) {
+        tools.compute(tag, (k, set) -> {
+            if (set == null) {
+                set = Collections.synchronizedSet(new LinkedHashSet<>());
+            }
+            synchronized (set) {
+                for (AiToolSpec existing : set) {
+                    if (existing.getName().equals(spec.getName()) && existing 
!= spec) {
+                        LOG.warn("Duplicate toolName '{}' under tag '{}' -- 
the LLM adapter will see both "

Review Comment:
   Warn-only on duplicate tool names means both specs stay registered, and most 
LLM APIs (OpenAI included) reject duplicate function names — so the failure 
surfaces later and far from the cause. Since this registry is the foundation 
for steps 2–6, consider failing fast at registration, or documenting last-wins 
semantics.



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolComponent.java:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.PropertiesHelper;
+import org.apache.camel.util.StringHelper;
+
+import static org.apache.camel.component.ai.tools.AiTool.SCHEME;
+
+/**
+ * Camel component that registers routes as LLM-callable tools in the shared 
{@link AiToolRegistry}.
+ *
+ * @since 4.22
+ */
+@Component(SCHEME)
+public class AiToolComponent extends DefaultComponent {
+
+    @Metadata(description = "The component configuration")
+    private AiToolConfiguration configuration;
+
+    public AiToolComponent() {
+        this(null);
+    }
+
+    public AiToolComponent(CamelContext context) {
+        super(context);
+        this.configuration = new AiToolConfiguration();
+    }
+
+    @Override
+    protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) throws Exception {
+        if (ObjectHelper.isEmpty(remaining)) {
+            throw new IllegalArgumentException(
+                    "A toolName must be provided: 
ai-tool:<toolName>?description=<desc>");
+        }
+
+        final String toolName = StringHelper.before(remaining, "/", remaining);

Review Comment:
   `ai-tool:myTool/anything` silently drops `/anything`. Since the syntax is 
`ai-tool:toolName` with no path segments, better to fail on a `/` (or use 
`remaining` as-is) so typos surface instead of registering a truncated tool 
name.



##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolRegistry.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.component.ai.tools;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * CamelContext-scoped registry mapping tags to {@link AiToolSpec} instances. 
AI components (LangChain4j, Spring AI)
+ * read from this registry to discover tools registered via the {@code 
ai-tool} consumer endpoint.
+ * <p>
+ * Each {@link CamelContext} gets its own registry instance, registered as a 
context plugin. Use
+ * {@link #getOrCreate(CamelContext)} to obtain the instance for a given 
context.
+ * <p>
+ * Replaces the duplicated {@code CamelToolExecutorCache} singletons from 
{@code camel-langchain4j-tools} and
+ * {@code camel-spring-ai-tools}.
+ *
+ * @since 4.22
+ */
+public final class AiToolRegistry {

Review Comment:
   This class relies on `synchronized` throughout — `synchronized (context)` in 
`getOrCreate`, `Collections.synchronizedSet`, and `synchronized (set)` blocks 
in `put`/`getToolsByTag`/`getAllTools`/`getTools`/`getDefaultTools`. Camel 
supports virtual threads, and the project has been systematically replacing 
`synchronized` with `ReentrantLock` for virtual-thread compatibility 
(CAMEL-20199, e.g. #21703) — new code shouldn't reintroduce it. A 
`ReentrantLock`-guarded `LinkedHashSet` keeps the insertion ordering, or 
`CopyOnWriteArraySet` would fit this read-heavy registry with no locking at all.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to