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 303f8c47852c CAMEL-26932: camel-jbang-mcp - a repeated catalog miss 
gets a note that says what the answer was, with its suggestions (#29962)
303f8c47852c is described below

commit 303f8c47852c454f0c5c97ee2dc5e50fe65fcfaa
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 20:51:39 2026 +0100

    CAMEL-26932: camel-jbang-mcp - a repeated catalog miss gets a note that 
says what the answer was, with its suggestions (#29962)
    
    CAMEL-26932: camel-jbang - RepeatedToolCalls.repeatOf computes the key of 
the call once
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../jbang/core/commands/ai/RepeatedToolCalls.java  | 49 ++++++++++++++++++++--
 .../core/commands/ai/RepeatedToolCallsTest.java    | 37 ++++++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |  2 +-
 .../dsl/jbang/core/commands/mcp/DiagnoseTools.java |  2 +-
 .../core/commands/mcp/RepeatedCallSessions.java    | 10 ++++-
 .../core/commands/mcp/AuthoringToolsTest.java      | 18 ++++++++
 6 files changed, 112 insertions(+), 6 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
index ca7f56b57ef7..276ae629601e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HexFormat;
 import java.util.Map;
@@ -59,6 +60,9 @@ public final class RepeatedToolCalls {
 
     private final Map<String, Integer> counts = new HashMap<>();
 
+    /** The calls whose answer was an error given as data, such as a name the 
catalog has not got, with that answer. */
+    private final Map<String, JsonObject> misses = new HashMap<>();
+
     /**
      * Returns the short note to answer with instead of running the tool, or 
null when the tool should run and answer in
      * full: a tool that is not deterministic, or one of the first {@value 
#FULL_ANSWERS} identical calls. When this
@@ -71,21 +75,41 @@ public final class RepeatedToolCalls {
         }
         if (counts.size() >= MAX_TRACKED) {
             counts.clear();
+            misses.clear();
         }
         // Peek at the current count without incrementing. The increment 
happens only after a successful answer
         // (recordSuccessfulAnswer), so a call that throws an exception does 
not consume the retry budget.
-        int current = counts.getOrDefault(key(tool.name(), args, 
tool.deterministicWhen()), 0);
+        String key = key(tool.name(), args, tool.deterministicWhen());
+        int current = counts.getOrDefault(key, 0);
         if (current < FULL_ANSWERS) {
             // Will be incremented by recordSuccessfulAnswer; answer in full.
             return null;
         }
         // Already answered FULL_ANSWERS times; the next identical call gets 
the note.
         // Increment so timesAsked keeps growing (allows the model to see the 
repetition count rising).
-        int times = counts.merge(key(tool.name(), args, 
tool.deterministicWhen()), 1, Integer::sum);
+        int times = counts.merge(key, 1, Integer::sum);
         JsonObject note = new JsonObject();
         note.put("repeated", true);
         note.put("tool", tool.name());
         note.put("timesAsked", times);
+        JsonObject miss = misses.get(key);
+        if (miss != null) {
+            // the hint of the tool is for an answer that was found, such as 
writing the file with the options it
+            // gave; a miss gives no options, so the note says what the answer 
was and what to ask instead (CAMEL-26932)
+            Object suggestions = miss.get("suggestions");
+            note.put("note", "The answer was: " + miss.getString("error") + ". 
"
+                             + (suggestions instanceof Collection<?> c && 
!c.isEmpty()
+                                     ? "Use one of the suggestions instead: " 
+ String.join(", ", c.stream()
+                                             .map(String::valueOf).toList())
+                                       + "."
+                                     : "Ask with other arguments instead.")
+                             + " You asked this " + times + " times now with 
the same arguments, and the answer is the"
+                             + " same each time.");
+            if (suggestions != null) {
+                note.put("suggestions", suggestions);
+            }
+            return note;
+        }
         // the next step first, then the fact; the count is timesAsked, this 
call included (CAMEL-26875)
         note.put("note", (tool.repeatHint() != null ? tool.repeatHint() : 
NEXT_STEP) + " You asked this " + times
                          + " times now with the same arguments, and the answer 
is the same each time: it is in your"
@@ -98,18 +122,37 @@ public final class RepeatedToolCalls {
      * {@value #FULL_ANSWERS} budget; calls that throw an exception do not.
      */
     public synchronized void recordSuccessfulAnswer(ToolDescriptor tool, 
Map<String, ?> args) {
+        recordSuccessfulAnswer(tool, args, null);
+    }
+
+    /**
+     * As {@link #recordSuccessfulAnswer(ToolDescriptor, Map)}, with the 
answer: an answer with an {@code error}, such
+     * as a name the catalog has not got, is a miss, and the note for 
repeating it says so instead of the hint of the
+     * tool. A miss still counts, so the model is still stopped from asking 
the same again and again (CAMEL-26875).
+     *
+     * @param answer the answer of the tool, or null when it is not JSON
+     */
+    public synchronized void recordSuccessfulAnswer(ToolDescriptor tool, 
Map<String, ?> args, JsonObject answer) {
         if (tool == null || !tool.isDeterministic(args)) {
             return;
         }
         if (counts.size() >= MAX_TRACKED) {
             counts.clear();
+            misses.clear();
+        }
+        String key = key(tool.name(), args, tool.deterministicWhen());
+        counts.merge(key, 1, Integer::sum);
+        if (answer != null && answer.get("error") instanceof String) {
+            misses.put(key, answer);
+        } else {
+            misses.remove(key);
         }
-        counts.merge(key(tool.name(), args, tool.deterministicWhen()), 1, 
Integer::sum);
     }
 
     /** Forgets the calls so far: a new session starts. */
     public synchronized void reset() {
         counts.clear();
+        misses.clear();
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
index c7bd71cb5a79..109938f5a82c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
@@ -63,6 +64,42 @@ class RepeatedToolCallsTest {
         assertThat(fourth.getString("note")).contains("You asked this 4 times 
now");
     }
 
+    @Test
+    void aRepeatedMissSaysWhatTheAnswerWasAndStillCounts() {
+        // the hint of the tool, to write the file with the options the answer 
gave, did not fit a miss (CAMEL-26932)
+        Map<String, String> kafak = Map.of("name", "kafak");
+        JsonObject miss = new JsonObject();
+        miss.put("error", "Artifact not found: kafak");
+        miss.put("suggestions", new JsonArray(List.of("kafka")));
+        for (int i = 0; i < 2; i++) {
+            assertThat(calls.repeatOf(catalogDoc, kafak)).isNull();
+            calls.recordSuccessfulAnswer(catalogDoc, kafak, miss);
+        }
+
+        JsonObject note = calls.repeatOf(catalogDoc, kafak);
+        assertThat(note.getBoolean("repeated")).isTrue();
+        assertThat(note.getInteger("timesAsked")).isEqualTo(3);
+        assertThat(note.getString("note"))
+                .startsWith("The answer was: Artifact not found: kafak. Use 
one of the suggestions instead: kafka.")
+                .contains("You asked this 3 times now")
+                .doesNotContain("camel_write_file");
+        assertThat(note.get("suggestions")).isEqualTo(List.of("kafka"));
+    }
+
+    @Test
+    void aRepeatedMissWithoutSuggestionsSaysToAskWithOtherArguments() {
+        Map<String, String> nope = Map.of("name", "nosuchthing");
+        JsonObject miss = new JsonObject();
+        miss.put("error", "Artifact not found: nosuchthing");
+        calls.recordSuccessfulAnswer(catalogDoc, nope, miss);
+        calls.recordSuccessfulAnswer(catalogDoc, nope, miss);
+
+        JsonObject note = calls.repeatOf(catalogDoc, nope);
+        assertThat(note.getString("note"))
+                .startsWith("The answer was: Artifact not found: nosuchthing. 
Ask with other arguments instead.");
+        assertThat(note.get("suggestions")).isNull();
+    }
+
     /**
      * r32, a Citrus test step: camel_catalog_sample had no sample of 'send 
message' as an EIP, and the model asked the
      * same 11 or 12 more times, until its tool calls were spent, without 
writing the test. The note starts with the
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index de114e097042..1e722486bfd6 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -537,7 +537,7 @@ public class AuthoringTools {
         }
         JsonObject result = call(tool, args, catalogService, configure);
         if (repeatedCalls != null) {
-            repeatedCalls.recordSuccessfulAnswer(connection, tool, args);
+            repeatedCalls.recordSuccessfulAnswer(connection, tool, args, 
result);
         }
         return result;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
index 0d6a7ac4a836..4fdeea8e0990 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
@@ -82,7 +82,7 @@ public class DiagnoseTools {
             CamelCatalog catalog = catalogService.loadCatalog(runtime, 
camelVersion, platformBom);
             JsonObject answer = ErrorDiagnoser.diagnose(error, catalog);
             if (repeatedCalls != null) {
-                repeatedCalls.recordSuccessfulAnswer(connection, 
"camel_error_diagnose", diagArgs);
+                repeatedCalls.recordSuccessfulAnswer(connection, 
"camel_error_diagnose", diagArgs, answer);
             }
             return answer;
         } catch (ToolCallException e) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RepeatedCallSessions.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RepeatedCallSessions.java
index be253d7c4134..8807ded9e691 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RepeatedCallSessions.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RepeatedCallSessions.java
@@ -65,6 +65,14 @@ public class RepeatedCallSessions {
      * budget (CAMEL-25640).
      */
     void recordSuccessfulAnswer(McpConnection connection, String tool, 
Map<String, String> args) {
+        recordSuccessfulAnswer(connection, tool, args, null);
+    }
+
+    /**
+     * As {@link #recordSuccessfulAnswer(McpConnection, String, Map)}, with 
the answer, so a repeated miss, such as a
+     * name the catalog has not got, gets a note of its own (CAMEL-26932).
+     */
+    void recordSuccessfulAnswer(McpConnection connection, String tool, 
Map<String, String> args, JsonObject answer) {
         if (connection == null || connection.id() == null) {
             return;
         }
@@ -73,7 +81,7 @@ public class RepeatedCallSessions {
             calls = sessions.get(connection.id());
         }
         if (calls != null) {
-            calls.recordSuccessfulAnswer(ToolRegistry.findTool(tool), args);
+            calls.recordSuccessfulAnswer(ToolRegistry.findTool(tool), args, 
answer);
         }
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index bdefe36df6ca..95a5f3196ec1 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -264,6 +264,24 @@ class AuthoringToolsTest {
         }
     }
 
+    @Test
+    void aThirdIdenticalCatalogMissGetsTheSuggestionsNotTheWriteHint() {
+        // CAMEL-26932: a miss is answered as data, so it still counts; its 
note says what to ask instead
+        AuthoringTools counted = new AuthoringTools();
+        counted.repeatedCalls = new RepeatedCallSessions();
+        McpConnection agent = connection("agent-1");
+
+        JsonObject first = counted.camel_catalog_doc(agent, "kafak", null, 
null, null, null, null, null, null, null, null);
+        counted.camel_catalog_doc(agent, "kafak", null, null, null, null, 
null, null, null, null, null);
+        JsonObject third = counted.camel_catalog_doc(agent, "kafak", null, 
null, null, null, null, null, null, null, null);
+
+        assertThat(first.getString("error")).contains("kafak");
+        assertThat(third.getBoolean("repeated")).isTrue();
+        assertThat(third.getString("note")).startsWith("The answer was: 
Artifact not found: kafak.")
+                .contains("Use one of the suggestions instead: kafka.")
+                .doesNotContain("camel_write_file");
+    }
+
     @Test
     void aThirdIdenticalCatalogCallOfAConnectionGetsAShortNote() {
         AuthoringTools counted = new AuthoringTools();

Reply via email to