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 353f408c9843 CAMEL-26721: camel-jbang - describe the chaos you want in 
your own words, and the AI turns it into checked rules (#29391)
353f408c9843 is described below

commit 353f408c98437d5be4588066b4de78a058c7f087
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 22:11:26 2026 +0100

    CAMEL-26721: camel-jbang - describe the chaos you want in your own words, 
and the AI turns it into checked rules (#29391)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-chaos-describe.png | Bin 0 -> 514742 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   8 +
 .../dsl/jbang/core/commands/ai/ToolConfirms.java   |  44 +-
 .../jbang/core/commands/ai/ToolConfirmsTest.java   |   6 +
 dsl/camel-jbang/camel-jbang-plugin-tui/README.md   |  35 ++
 dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml     |   7 +
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  11 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   8 +
 .../dsl/jbang/core/commands/tui/ChaosAiPrompt.java | 100 ++++
 .../dsl/jbang/core/commands/tui/ChaosControl.java  |  44 +-
 .../jbang/core/commands/tui/MonitorContext.java    |   9 +
 .../core/commands/tui/TuiToolDefinitions.java      |  28 +
 .../dsl/jbang/core/commands/tui/TuiToolGroups.java |  17 +-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  46 +-
 .../src/main/resources/tui/help/chaos.md           |   9 +
 .../core/commands/tui/AiPanelToolGroupsTest.java   |  11 +-
 .../jbang/core/commands/tui/ChaosAiPromptTest.java | 102 ++++
 .../core/commands/tui/ChaosRequestEvalTest.java    | 564 +++++++++++++++++++++
 .../core/commands/tui/McpFacadeConfirmTest.java    |  26 +
 .../jbang/core/commands/tui/SourceSizeTest.java    |   2 +-
 .../src/test/resources/chaos-eval/requests.json    | 277 ++++++++++
 21 files changed, 1337 insertions(+), 17 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png
new file mode 100644
index 000000000000..9e7c66382c0b
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index 74d535fe0f63..4587aefcc762 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -330,6 +330,14 @@ the route, and of its polls when it polls.
 image::jbang/camel-tui-chaos-here.png[! on the to callStock of the Diagram: 
the known rules for its HTTP endpoint and its step, with the endpoint filled in]
 * *x* removes the selected rule, *c* removes all rules (the report keeps what 
they did), and *R* starts a new experiment:
 no rules and a new report, with the same seed.
+* *d* describes the chaos with AI, when the AI panel is there: say the chaos 
you want in your own words, such as "the
+order route fails 5% of the time, or when the country is Spain". Enter sends 
it to the AI panel with the routes, the
+sends and the rules of the integration. The AI writes the rules, checks them 
with `tui_chaos_check` (each in plain
+words, and what it would hit), shows them, and adds them with `tui_chaos_add` 
(or changes a rule with
+`tui_chaos_change`, which keeps its id and gives it a new version), asking you 
to confirm each one. It does not change
+the routes. The AI panel has these tools only while chaos is on in the 
integration.
++
+image::jbang/camel-tui-chaos-describe.png[Describe with AI: the AI checked the 
two rules of "the order route should fail 5% of the time, or when the country 
is spain", and asks to add them]
 
 Each asks to confirm with *y*, with the rule shown. A rule that is not valid 
comes back with the reason, and a rule that
 matches nothing in the integration (often a typo) with a warning; press *e* 
there to change it.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
index 855e184a08fe..771b93ebcfd1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
@@ -303,14 +303,56 @@ public final class ToolConfirms {
         String where = integration != null && !integration.isBlank() ? 
integration : "the selected integration";
         String reason = "While the rule is active, the integration's messages 
fail, wait or get lost; exception= can name"
                         + " any exception class";
+        List<String> rules = c.isEmpty() ? List.of() : splitRules(rule);
         return switch (c) {
-            case "add" -> new Call("add a chaos rule to " + where, rule, 
reason);
+            // several rules separated by ; are added together: one per line, 
so each can be read
+            case "add" -> rules.size() > 1
+                    ? new Call("add " + rules.size() + " chaos rules to " + 
where, String.join("\n", rules), reason)
+                    : new Call("add a chaos rule to " + where, rule, reason);
             case "change" -> id != null && !id.isBlank()
                     ? new Call("change chaos rule " + id + " of " + where, 
rule, reason) : null;
             default -> null;
         };
     }
 
+    /**
+     * Splits a text with several rules separated by {@code ;}, as in 
camel.chaos.rules; a {@code ;} inside quotes does
+     * not split.
+     */
+    static List<String> splitRules(String text) {
+        List<String> answer = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        for (int i = 0; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            if (quote != 0) {
+                current.append(ch);
+                if (ch == '\\' && i + 1 < text.length()) {
+                    current.append(text.charAt(++i));
+                } else if (ch == quote) {
+                    quote = 0;
+                }
+            } else if (ch == ';') {
+                addRule(answer, current);
+            } else {
+                if (ch == '"' || ch == '\'') {
+                    quote = ch;
+                }
+                current.append(ch);
+            }
+        }
+        addRule(answer, current);
+        return answer;
+    }
+
+    private static void addRule(List<String> rules, StringBuilder text) {
+        String rule = text.toString().strip();
+        if (!rule.isEmpty()) {
+            rules.add(rule);
+        }
+        text.setLength(0);
+    }
+
     /** Running an integration on this machine. */
     public static Call run(String what) {
         return new Call("run an integration", what, "It starts an integration 
on this machine with the project's code");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
index 63e11ca544ca..f868d9b950e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
@@ -292,6 +292,12 @@ class ToolConfirmsTest {
         assertThat(add.target()).isEqualTo("endpoint=* error 
exception=java.sql.SQLException");
         assertThat(add.reason()).contains("fail, wait or get lost", 
"exception=");
 
+        // several rules are added together: one per line, and a ; inside 
quotes does not separate
+        ToolConfirms.Call two = ToolConfirms.chaos("add", "chaosdemo", null,
+                "route=orders error rate=0.05; route=orders error 
when=\"${header.note} == 'a;b'\"");
+        assertThat(two.title()).isEqualTo("add 2 chaos rules to chaosdemo");
+        assertThat(two.target()).isEqualTo("route=orders error 
rate=0.05\nroute=orders error when=\"${header.note} == 'a;b'\"");
+
         ToolConfirms.Call change = ToolConfirms.chaos("change", null, "r1", 
"endpoint=* drop");
         assertThat(change.title()).isEqualTo("change chaos rule r1 of the 
selected integration");
         assertThat(change.target()).isEqualTo("endpoint=* drop");
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/README.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/README.md
index 2d357e021f5f..846e15b5324b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/README.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/README.md
@@ -79,5 +79,40 @@ How to read it:
   request takes about a minute. Mixture-of-experts models such as 
`qwen3.6:35b-a3b` are several times
   faster, which is why the TUI recommends them.
 
+### `ChaosRequestEvalTest` (opt-in)
+
+The eval set of "describe the chaos you want" (`d` on the Chaos screen): 18 
loose requests in
+`src/test/resources/chaos-eval/requests.json`, such as "the order route should 
fail 5% of the time, or
+when the country is spain", each with the rules it should become. Each request 
goes to a local model as
+the AI panel sends it (its system prompt, the core tools and the chaos tools, 
and the question of
+`ChaosAiPrompt.ofRequest`). camel-chaos answers `tui_chaos_check` over routes 
shaped like the chaos
+demo; the rules the model adds or changes are recorded, as if the user 
confirmed them, and scored: each
+expected rule must match one rule (its target, fault and options as regular 
expressions, `null` for an
+option that must be absent, the texts of its `when=`), and no rule may be left 
over. It is skipped unless
+`CAMEL_TUI_CHAOS_EVAL` names the model:
+
+```bash
+CAMEL_TUI_CHAOS_EVAL=qwen3.6:35b-mlx mvn test -Dtest=ChaosRequestEvalTest
+```
+
+`CAMEL_TUI_CHAOS_EVAL_RUNS=3` runs each request three times, 
`CAMEL_TUI_CHAOS_EVAL_ONLY=<id>` runs only
+the requests whose id contains it, and `CAMEL_TUI_CHAOS_EVAL_VERBOSE=true` 
prints each tool call. It
+measures a model and the prompt; it does not fail on a wrong answer. A line 
per request and a summary:
+
+```
+FAIL fail-or-spain             12.0s  2 calls  1 checks  14177 tok  rate=0.05, 
expected none in [route=orders error rate=0.05]
+PASS stock-503                 13.0s  3 calls  1 checks  28456 tok  
[endpoint=http://localhost:8765/stock error status=503 rate=0.2]
+...
+7 of 18 passed (39%), 11.1s and 4 tool calls on average
+```
+
+With `qwen3.6:35b-mlx` (Apple M4 Pro, 2026-11-07) single runs pass 6 or 7 of 
18; one run of the same
+prompt can differ from the next by several requests, so compare a change of 
the prompt over three runs.
+The known rules of the catalog in the question took it from 3 to 6: timeouts, 
stalled routes, the polls
+of a consumer and outages then come out right. What it still gets wrong: the 
same fault at two places
+(the service and the route that calls it), a value of the examples 
(`rate=0.05`) in rules that did not
+ask for one, `drop` for "cut in half" (`corrupt mode=truncate`), and a check 
repeated with the same
+mistake.
+
 The same figures are available live inside the TUI: `/context` in the F8 panel 
shows the prefix and
 history size of the next request, and `/usage` the tokens and latency spent so 
far.
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index ca0f2b248143..e2c0f2e01cd4 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -115,6 +115,13 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <!-- the eval of the chaos requests answers the check tool with 
camel-chaos itself -->
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-chaos</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-test-junit6</artifactId>
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 036db0262b07..f3f75f40c949 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
@@ -598,6 +598,17 @@ class AiPanel {
         inputLine.replace(question);
     }
 
+    /**
+     * Opens the panel and sends the question, which the user typed and 
confirmed on a screen. While the panel answers
+     * another question, or asks about an edit, the question waits in the 
input for the user to send.
+     */
+    void askNow(String question) {
+        openWithQuestion(question);
+        if (!thinking.get() && editQuestionHandler == null && activeCliCommand 
== null) {
+            submitInput();
+        }
+    }
+
     boolean isAskingAboutEdit() {
         return editQuestionHandler != null;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index dc41edae958f..d53884bacde7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -601,6 +601,14 @@ public class CamelMonitor extends CamelCommand {
             public void ask(String question) {
                 openAiWithQuestion(dir -> question);
             }
+
+            @Override
+            public void askNow(String question) {
+                if (shellPanel.isOpen()) {
+                    shellPanel.close();
+                }
+                aiPanel.askNow(question);
+            }
         };
         ctx.projectOverviewCallback = () -> {
             if (shellPanel.isOpen()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
index 552ee7baeaaf..b9a0fb741d0a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
@@ -48,6 +48,106 @@ final class ChaosAiPrompt {
         return sb.toString();
     }
 
+    /** The most routes and sends quoted in a request, so a big integration 
does not flood it. */
+    static final int MAX_LISTED = 20;
+
+    /**
+     * The question for a request in the user's own words ({@code d} on the 
Chaos screen, CAMEL-26721), such as "the
+     * order route should fail 5% of the time": the request, the routes and 
the sends of the integration and its rules
+     * now, the rule syntax in short, and what to do - write the rules, check 
them with tui_chaos_check (which says what
+     * each would hit, in plain words), show them, and add them only with 
tui_chaos_add (or change one with
+     * tui_chaos_change), which ask the user first.
+     */
+    static String ofRequest(IntegrationInfo info, String request) {
+        return ofRequest(info, request, null);
+    }
+
+    /**
+     * The question for a request in the user's own words, with the known 
rules of camel-chaos (the recipes of the chaos
+     * model, a line each with its rule), which show the forms a short syntax 
leaves out: a timeout, a stalled route,
+     * the polls of a consumer, the dead letter channel.
+     *
+     * @param recipes the known rules, or null when the catalog has none
+     */
+    static String ofRequest(IntegrationInfo info, String request, ChaosRecipes 
recipes) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Turn my request into camel-chaos rules for the running 
integration ").append(name(info))
+                .append(" (development only, faults injected on purpose). My 
request: ").append(request.strip())
+                .append('\n');
+        int listed = 0;
+        StringBuilder routes = new StringBuilder();
+        for (RouteInfo route : info.routes) {
+            if (route.routeId != null && listed++ < MAX_LISTED) {
+                routes.append("  ").append(route.routeId);
+                if (route.from != null) {
+                    routes.append(" from ").append(route.from);
+                }
+                routes.append('\n');
+            }
+        }
+        if (!routes.isEmpty()) {
+            sb.append("Its routes:\n").append(routes);
+        }
+        // the endpoint a route consumes is not one of its sends (a send to it 
from another route is)
+        List<String> consumed = new ArrayList<>();
+        info.routes.forEach(r -> {
+            if (r.from != null && r.routeId != null) {
+                consumed.add(r.routeId + " " + sameUri(r.from));
+            }
+        });
+        List<String> sends = new ArrayList<>();
+        for (EndpointInfo endpoint : info.endpoints) {
+            if ("out".equals(endpoint.direction) && endpoint.uri != null && 
sends.size() < MAX_LISTED
+                    && !consumed.contains(endpoint.routeId + " " + 
sameUri(endpoint.uri))) {
+                String send = withoutQuery(endpoint.uri) + (endpoint.routeId 
!= null ? " (from " + endpoint.routeId + ")" : "");
+                if (!sends.contains(send)) {
+                    sends.add(send);
+                }
+            }
+        }
+        if (!sends.isEmpty()) {
+            sb.append("It sends to:\n");
+            sends.forEach(s -> sb.append("  ").append(s).append('\n'));
+        }
+        ChaosInfo chaos = info.chaos;
+        if (chaos != null && !chaos.rules.isEmpty()) {
+            sb.append("Its chaos rules now, which are active already (add only 
new rules; change one of these with ")
+                    .append(TuiToolGroups.CHAOS_CHANGE_TOOL).append(" and its 
id):\n");
+            chaos.rules.forEach(r -> sb.append("  ").append(r.id).append(": 
").append(r.text).append('\n'));
+        }
+        sb.append("A rule is one line: <target> <fault> [<parameter>=<value> 
...] [<option>=<value> ...]. Targets:")
+                .append(" endpoint=<uri pattern>, component=<scheme>, 
route=<route id>, step=<step id>, with * for any")
+                .append(" characters. Faults: error (exception=<class>, 
message=, status=<http code> for http),")
+                .append(" delay (duration=500ms or 100ms..2s), drop, corrupt 
(mode=headers|charset|truncate|empty),")
+                .append(" duplicate, stall (route=, duration=). Options: 
rate=0.05 (or every:N), until=10m,")
+                .append(" in=<route id> for the sends and steps of a route, 
when=\"${header.country} == 'ES'\" for")
+                .append(" the messages that meet a condition (simple 
language). route=<id> error fails 5% of the")
+                .append(" exchanges of a route with rate=0.05; \"or\" is two 
rules. Each rule has its own rate: without")
+                .append(" rate= a rule fails every message it matches.\n");
+        if (recipes != null && !recipes.recipes().isEmpty()) {
+            sb.append("Known rules, with ${...} to fill in:\n");
+            recipes.recipes().forEach(r -> sb.append("  
").append(r.title()).append(": ").append(r.rule()).append('\n'));
+        }
+        sb.append("The routes and sends above are all you need: do not look 
around with other tools. First call ")
+                .append(TuiToolGroups.CHAOS_CHECK_TOOL).append(" with the 
rules (several separated by ;): it says each")
+                .append(" rule in plain words and what it would hit, or why it 
is not valid. Fix the rules until it")
+                .append(" accepts them and they hit what I asked for. Then 
show me each rule with its plain words and")
+                .append(" what it hits, and add them with 
").append(TuiToolGroups.CHAOS_ADD_TOOL)
+                .append(" (or change a rule above with 
").append(TuiToolGroups.CHAOS_CHANGE_TOOL)
+                .append("), which asks me to confirm first. Do not change the 
routes.");
+        return sb.toString();
+    }
+
+    // a URI without its query and the // after its scheme, to tell the 
endpoint a route consumes
+    private static String sameUri(String uri) {
+        return withoutQuery(uri).replaceFirst(":/+", ":");
+    }
+
+    private static String withoutQuery(String uri) {
+        int query = uri.indexOf('?');
+        return query > 0 ? uri.substring(0, query) : uri;
+    }
+
     /** The question for an error a chaos rule made, on the Errors tab: it is 
not a bug to fix. */
     static String ofError(IntegrationInfo info, ChaosInfo.Rule rule, String 
ruleId, ErrorInfo error) {
         StringBuilder sb = new StringBuilder();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
index 502211cb0e5b..b28382b46a26 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
@@ -65,7 +65,10 @@ final class ChaosControl {
         CHANGE,
         REMOVE,
         CLEAR,
-        RESET
+        RESET,
+        // the chaos the user wants, in their own words, for the AI panel to 
turn into rules (not sent to the
+        // integration)
+        DESCRIBE
     }
 
     private final MonitorContext ctx;
@@ -87,6 +90,8 @@ final class ChaosControl {
     private String resultTitle;
     private final List<String> resultLines = new ArrayList<>();
     private boolean resultError;
+    // the integration the user describes the chaos for
+    private IntegrationInfo describing;
 
     ChaosControl(MonitorContext ctx) {
         this(ctx, null, ChaosRecipes::load);
@@ -200,6 +205,12 @@ final class ChaosControl {
             ask(Command.RESET);
             return true;
         }
+        if (ke.isChar('d') && ctx.askAiCallback != null) {
+            // the chaos the user wants in their own words, which the AI turns 
into rules, checks and adds (CAMEL-26721)
+            describing = info;
+            openInput(Command.DESCRIBE, "");
+            return true;
+        }
         return false;
     }
 
@@ -260,7 +271,13 @@ final class ChaosControl {
             close();
         } else if (ke.isConfirm()) {
             String text = input.text().strip();
-            if (!text.isEmpty()) {
+            if (!text.isEmpty() && command == Command.DESCRIBE) {
+                // the AI panel asks right away: the user typed the request 
and pressed Enter; the rules it makes are
+                // added only through the add tool, which asks the user first
+                IntegrationInfo info = describing;
+                close();
+                ctx.askAiCallback.askNow(ChaosAiPrompt.ofRequest(info, text, 
knownRules()));
+            } else if (!text.isEmpty()) {
                 rule = text;
                 ask(command);
             }
@@ -310,6 +327,7 @@ final class ChaosControl {
         step = Step.NONE;
         input = null;
         form = null;
+        describing = null;
     }
 
     /** The request of the chaos action for the command, as {@code camel cmd 
chaos-*} sends it. */
@@ -376,6 +394,8 @@ final class ChaosControl {
             case REMOVE -> "Removed chaos rule " + id;
             case CLEAR -> "Removed all chaos rules";
             case RESET -> "New chaos experiment: no rules, a new report";
+            // never sent to the integration
+            case DESCRIBE -> "";
         };
         if (reply.get("warnings") instanceof List<?> warnings && 
!warnings.isEmpty()) {
             List<String> lines = new ArrayList<>();
@@ -424,9 +444,16 @@ final class ChaosControl {
     void render(Frame frame, Rect area) {
         switch (step) {
             case RECIPES -> form.render(frame, area);
-            case INPUT -> DialogHelper.renderInputDialog(frame, area,
-                    command == Command.CHANGE ? "Change chaos rule " + id : 
"Add a chaos rule to " + name, input,
-                    "endpoint=kafka:orders* error rate=0.1 until=5m");
+            case INPUT -> {
+                if (command == Command.DESCRIBE) {
+                    DialogHelper.renderInputDialog(frame, area, "Describe the 
chaos you want in " + name, input,
+                            "the order route fails 5% of the time, or when the 
country is Spain");
+                } else {
+                    DialogHelper.renderInputDialog(frame, area,
+                            command == Command.CHANGE ? "Change chaos rule " + 
id : "Add a chaos rule to " + name, input,
+                            "endpoint=kafka:orders* error rate=0.1 until=5m");
+                }
+            }
             case CONFIRM -> DialogHelper.renderConfirm(frame, area, title(), 
question(), detail(), "y", "yes", null, null);
             case RESULT -> renderResult(frame, area);
             default -> {
@@ -442,6 +469,7 @@ final class ChaosControl {
             case REMOVE -> "Remove Chaos Rule";
             case CLEAR -> "Clear Chaos Rules";
             case RESET -> "New Chaos Experiment";
+            case DESCRIBE -> "Describe the Chaos";
         };
     }
 
@@ -453,6 +481,7 @@ final class ChaosControl {
             case REMOVE -> "Remove chaos rule " + id + " from " + name + "?";
             case CLEAR -> "Remove all chaos rules from " + name + "? The 
report keeps what they did";
             case RESET -> "Start a new chaos experiment in " + name + "? No 
rules, and a new report (same seed)";
+            case DESCRIBE -> "Ask the AI to turn this into chaos rules?";
         };
     }
 
@@ -501,7 +530,7 @@ final class ChaosControl {
         switch (step) {
             case RECIPES -> form.renderFooter(spans);
             case INPUT -> {
-                TuiHelper.hint(spans, "Enter", "confirm");
+                TuiHelper.hint(spans, "Enter", command == Command.DESCRIBE ? 
"ask the AI" : "confirm");
                 TuiHelper.hint(spans, "Esc", "cancel");
             }
             case CONFIRM -> {
@@ -518,6 +547,9 @@ final class ChaosControl {
                     TuiHelper.hint(spans, "c", "clear");
                 }
                 TuiHelper.hint(spans, "R", "new experiment");
+                if (ctx.askAiCallback != null) {
+                    TuiHelper.hint(spans, "d", "describe with AI");
+                }
             }
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index e993f61030b6..82fe1c37e685 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -407,5 +407,14 @@ class MonitorContext {
          */
         default void ask(String question) {
         }
+
+        /**
+         * Opens the AI panel and sends the question at once, for a request 
the user typed and confirmed on a screen
+         * ({@code d} on the Chaos screen). When the panel is busy, the 
question waits in its input, as with
+         * {@link #ask(String)}.
+         */
+        default void askNow(String question) {
+            ask(question);
+        }
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 2e06525cf032..129b830242c2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -50,6 +50,7 @@ final class TuiToolDefinitions {
         List<TuiTool> tools = new ArrayList<>();
         addInteractionTools(tools, r);
         addStructuredDataTools(tools, r);
+        addChaosTools(tools, r);
         addDiagramTools(tools, r);
         addSharedTools(tools, r);
         addCatalogTools(tools, r);
@@ -294,6 +295,33 @@ final class TuiToolDefinitions {
                 r::callDiagramZoom));
     }
 
+    /**
+     * The chaos rules of the selected integration (camel-chaos), which only 
its tool group adds when chaos is on: check
+     * a rule, and add or change one after the user confirms (CAMEL-26721).
+     */
+    private static void addChaosTools(List<TuiTool> tools, TuiToolRegistry r) {
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_CHECK_TOOL,
+                "Checks chaos rules without adding them: each rule in plain 
words, where it can fire, warnings, and "
+                                                + "what it would hit now 
(hits: sends, polls, routes or steps with "
+                                                + "their route); a rule that 
is not valid comes back with the reason.",
+                Map.of("rule", propDef("string", "One-line rule, or several 
separated by ;")),
+                List.of("rule")), r::callChaosCheck, READ_ONLY));
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_ADD_TOOL,
+                "Adds chaos rules to the selected integration after the user 
confirms; returns their ids and plain "
+                                              + "words. Check them with " + 
TuiToolGroups.CHAOS_CHECK_TOOL + " first.",
+                Map.of("rule", propDef("string", "One-line rule, or several 
separated by ;")),
+                List.of("rule")), r::callChaosAdd));
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_CHANGE_TOOL,
+                "Changes a chaos rule after the user confirms: the rule with 
the id gets the new rule and its next "
+                                                 + "version; the report keeps 
the old one.",
+                Map.of("id", propDef("string", "The id of the rule, such as 
r1"),
+                        "rule", propDef("string", "The new one-line rule")),
+                List.of("id", "rule")), r::callChaosChange));
+    }
+
     private static void addStructuredDataTools(List<TuiTool> tools, 
TuiToolRegistry r) {
         tools.add(tool(toolDef(
                 "tui_get_table",
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
index 070491a70878..523dbc905645 100644
--- 
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
@@ -37,6 +37,9 @@ final class TuiToolGroups {
     static final String UPDATE_ROW_TOOL = "tui_update_row";
     static final String HTTP_ENDPOINTS_TOOL = "tui_http_endpoints";
     static final String HTTP_REQUEST_TOOL = "tui_http_request";
+    static final String CHAOS_CHECK_TOOL = "tui_chaos_check";
+    static final String CHAOS_ADD_TOOL = "tui_chaos_add";
+    static final String CHAOS_CHANGE_TOOL = "tui_chaos_change";
 
     /**
      * The groups of an integration as the panel uses them.
@@ -78,6 +81,11 @@ final class TuiToolGroups {
                 if (f.semanticMonitoring()) {
                     tools.add("tui_get_monitoring");
                 }
+            } else if (group == ToolGroup.CHAOS) {
+                // with chaos off the rules can only be refused
+                if (f.chaosOn()) {
+                    tools.addAll(tools(group));
+                }
             } else {
                 tools.addAll(tools(group));
             }
@@ -91,6 +99,7 @@ final class TuiToolGroups {
             case SQL -> List.of(SQL_TOOL, UPDATE_ROW_TOOL);
             case HTTP -> List.of(HTTP_ENDPOINTS_TOOL, HTTP_REQUEST_TOOL);
             case SEMANTIC -> List.of("tui_get_audit", "tui_get_monitoring");
+            case CHAOS -> List.of(CHAOS_CHECK_TOOL, CHAOS_ADD_TOOL, 
CHAOS_CHANGE_TOOL);
             default -> List.of();
         };
     }
@@ -119,11 +128,13 @@ final class TuiToolGroups {
                                + " rate; OPEN means the fallback runs.";
             case HTTP -> "HTTP: " + ToolGroups.describeHttp(f) + "; " + 
HTTP_REQUEST_TOOL + " calls it, "
                          + HTTP_ENDPOINTS_TOOL + " lists the operations.";
-            // the panel has no chaos tools (camel ask and camel-jbang-mcp 
have them): it only learns that faults may be
-            // injected on purpose, so it does not hunt for a bug behind them
+            // faults may be injected on purpose, so the panel does not hunt 
for a bug behind them; it checks, adds and
+            // changes rules as camel ask does, adding and changing after the 
user confirms (CAMEL-26721)
             case CHAOS -> f.chaosOn()
                     ? "Chaos: camel-chaos is on, so some failures and delays 
may be injected on purpose; tui_get_status"
-                      + " section 'chaos' has the rules, tui_get_table tab 
'Chaos' their live state and effect."
+                      + " section 'chaos' has the rules, tui_get_table tab 
'Chaos' their live state and effect; "
+                      + CHAOS_CHECK_TOOL + " checks rules, " + CHAOS_ADD_TOOL 
+ " and " + CHAOS_CHANGE_TOOL
+                      + " add and change them after the user confirms."
                     : "Chaos: camel-chaos is in the integration but off, so it 
injects no faults.";
         };
     }
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 63d187fea689..1d6d3f929bd4 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
@@ -259,10 +259,14 @@ class TuiToolRegistry {
         return tools;
     }
 
+    // the tools that only the tool groups of the integration add, also to the 
full set: the semantic history, and
+    // the chaos rules when camel-chaos is on
+    private static final Set<String> GROUP_ONLY_TOOLS = 
Set.of("tui_get_audit", "tui_get_monitoring",
+            TuiToolGroups.CHAOS_CHECK_TOOL, TuiToolGroups.CHAOS_ADD_TOOL, 
TuiToolGroups.CHAOS_CHANGE_TOOL);
+
     List<ToolDef> getAvailableToolDefinitions(Collection<String> selected) {
         return getToolDefinitions().stream()
-                .filter(tool -> !Set.of("tui_get_audit", 
"tui_get_monitoring").contains(tool.name())
-                        || selected.contains(tool.name()))
+                .filter(tool -> !GROUP_ONLY_TOOLS.contains(tool.name()) || 
selected.contains(tool.name()))
                 .toList();
     }
 
@@ -1341,6 +1345,44 @@ class TuiToolRegistry {
         return Jsoner.serialize(response);
     }
 
+    /** Checks chaos rules in the selected integration (chaos_check), which 
adds nothing. */
+    String callChaosCheck(Map<String, Object> args) {
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (rule == null || rule.isBlank()) {
+            return "Error: rule is required";
+        }
+        return executeShared("chaos_check", Map.of("rule", rule));
+    }
+
+    /** Adds chaos rules to the selected integration (chaos_add), after the 
user confirms. */
+    String callChaosAdd(Map<String, Object> args) {
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (rule == null || rule.isBlank()) {
+            return "Error: rule is required";
+        }
+        JsonObject rejected = facade != null
+                ? facade.confirm(ToolConfirms.chaos("add", 
facade.getSelectedIntegrationName(), null, rule)) : null;
+        if (rejected != null) {
+            return Jsoner.serialize(rejected);
+        }
+        return executeShared("chaos_add", Map.of("rule", rule));
+    }
+
+    /** Changes a chaos rule of the selected integration (chaos_change), after 
the user confirms. */
+    String callChaosChange(Map<String, Object> args) {
+        String id = args.get("id") instanceof String s ? s : null;
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (id == null || id.isBlank() || rule == null || rule.isBlank()) {
+            return "Error: id and rule are required";
+        }
+        JsonObject rejected = facade != null
+                ? facade.confirm(ToolConfirms.chaos("change", 
facade.getSelectedIntegrationName(), id, rule)) : null;
+        if (rejected != null) {
+            return Jsoner.serialize(rejected);
+        }
+        return executeShared("chaos_change", Map.of("id", id, "rule", rule));
+    }
+
     String callUpdateRow(Map<String, Object> args) {
         String table = (String) args.get("table");
         if (table == null || table.isBlank()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
index bc110d1593df..96e4daa87e73 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
@@ -77,6 +77,15 @@ error handler (HAND) and the redeliveries (REDEL).
   says what the fault simulates and how the routes coped, and suggests how
   to make them cope better, changing the routes only when you agree.
   Press Enter to send the question, or change it first
+- **d** — Describe with AI: say the chaos you want in your own words, such
+  as "the order route fails 5% of the time, or when the country is Spain".
+  Enter sends it to the AI panel with the routes, the sends and the rules
+  of the integration. The AI writes the rules, checks them with
+  `tui_chaos_check` (each rule in plain words, and what it would hit),
+  shows them to you, and adds them with `tui_chaos_add` (or changes a rule
+  with `tui_chaos_change`, which keeps its id and gives it a new version),
+  asking you to confirm each one. It does not change the routes. The AI
+  panel has these tools only while chaos is on in the integration
 
 Each asks to confirm with **y**, showing the rule. The integration checks
 the rule as `camel.chaos.rules` is checked: a rule that is not valid comes
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
index 666fe4879c02..663c0165c508 100644
--- 
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
@@ -236,19 +236,22 @@ class AiPanelToolGroupsTest {
     }
 
     @Test
-    void theChaosGroupAddsOnlyALine() {
-        // the panel has no chaos tools; its model learns that faults may be 
injected on purpose
+    void theChaosGroupAddsTheRuleToolsWhenChaosIsOn() {
+        // its model learns that faults may be injected on purpose, and 
checks, adds and changes rules (CAMEL-26721)
         TuiToolGroups.Selection on = TuiToolGroups.select(
                 AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", true)))));
         assertEquals(List.of(ToolGroup.CHAOS), on.groups());
-        assertTrue(on.tools().isEmpty(), on.tools().toString());
+        assertEquals(List.of("tui_chaos_check", "tui_chaos_add", 
"tui_chaos_change"), on.tools());
         assertEquals(List.of("Chaos: camel-chaos is on, so some failures and 
delays may be injected on purpose;"
                              + " tui_get_status section 'chaos' has the rules, 
tui_get_table tab 'Chaos' their live"
-                             + " state and effect."),
+                             + " state and effect; tui_chaos_check checks 
rules, tui_chaos_add and tui_chaos_change add"
+                             + " and change them after the user confirms."),
                 on.guidance());
 
+        // with chaos off a rule can only be refused: no tools
         TuiToolGroups.Selection off = TuiToolGroups.select(
                 AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", false)))));
+        assertTrue(off.tools().isEmpty(), off.tools().toString());
         assertEquals(List.of("Chaos: camel-chaos is in the integration but 
off, so it injects no faults."),
                 off.guidance());
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
index b3f5a7f5c355..c2b6f36dbe9d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
@@ -78,9 +78,111 @@ class ChaosAiPromptTest {
             public void ask(String question) {
                 asked.add(question);
             }
+
+            @Override
+            public void askNow(String question) {
+                asked.add("now: " + question);
+            }
         };
     }
 
+    @Test
+    void aRequestInTheUsersWordsWithTheRoutesTheSendsAndTheRules() {
+        RouteInfo orders = new RouteInfo();
+        orders.routeId = "orders";
+        orders.from = "timer://orders?period=500";
+        info.routes.add(orders);
+        EndpointInfo stock = new EndpointInfo();
+        stock.uri = "http://localhost:8765/stock?bridgeEndpoint=true";;
+        stock.direction = "out";
+        stock.routeId = "orders";
+        info.endpoints.add(stock);
+        EndpointInfo in = new EndpointInfo();
+        in.uri = "timer://orders";
+        in.direction = "in";
+        info.endpoints.add(in);
+        // a route that serves HTTP: its own endpoint is not a send, a send to 
it from orders is
+        RouteInfo stockRoute = new RouteInfo();
+        stockRoute.routeId = "stock";
+        stockRoute.from = "platform-http:/stock";
+        info.routes.add(stockRoute);
+        EndpointInfo served = new EndpointInfo();
+        served.uri = "platform-http:///stock";;
+        served.direction = "out";
+        served.routeId = "stock";
+        info.endpoints.add(served);
+        EndpointInfo direct = new EndpointInfo();
+        direct.uri = "direct://inventory";
+        direct.direction = "out";
+        direct.routeId = "orders";
+        info.endpoints.add(direct);
+
+        String question = ChaosAiPrompt.ofRequest(info, " the order route 
fails 5% of the time, or for Spain ");
+
+        assertThat(question).startsWith("Turn my request into camel-chaos 
rules for the running integration chaosdemo"
+                                        + " (development only, faults injected 
on purpose). My request: the order route"
+                                        + " fails 5% of the time, or for 
Spain\n")
+                .contains("Its routes:\n  orders from 
timer://orders?period=500\n")
+                // the sends, without their query; not the consumers
+                .contains("It sends to:\n  http://localhost:8765/stock (from 
orders)\n  direct://inventory (from orders)\n")
+                .doesNotContain("platform-http:///stock (from stock)")
+                .doesNotContain("bridgeEndpoint").doesNotContain("  
timer://orders (from")
+                .contains("Its chaos rules now, which are active already (add 
only new rules; change one of these with"
+                          + " tui_chaos_change and its id):\n  r1: 
endpoint=http://localhost:8765/stock* error status=503")
+                .contains("in=<route id>").contains("when=\"${header.country} 
== 'ES'\"").contains("\"or\" is two rules")
+                .contains("Each rule has its own rate: without rate= a rule 
fails every message it matches.")
+                .contains("do not look around with other 
tools").contains("First call tui_chaos_check with the rules")
+                .endsWith("add them with tui_chaos_add (or change a rule above 
with tui_chaos_change), which asks me to"
+                          + " confirm first. Do not change the routes.");
+    }
+
+    @Test
+    void theKnownRulesShowTheFormsTheShortSyntaxLeavesOut() {
+        ChaosRecipes recipes = ChaosRecipes.parse(ChaosRecipesTest.MODEL);
+        String question = ChaosAiPrompt.ofRequest(info, "calls to stock time 
out", recipes);
+        assertThat(question).contains("Known rules, with ${...} to fill 
in:\n");
+        recipes.recipes().forEach(r -> assertThat(question).contains("  " + 
r.title() + ": " + r.rule() + "\n"));
+        // without a catalog that has them, none
+        assertThat(ChaosAiPrompt.ofRequest(info, "calls to stock time 
out")).doesNotContain("Known rules");
+    }
+
+    @Test
+    void dOnTheChaosScreenAsksTheAiWithTheRequest() {
+        ChaosTab tab = new ChaosTab(ctx);
+        TuiTestHelper.renderToString(tab, 160, 40);
+        List<Span> footer = new ArrayList<>();
+        tab.renderFooter(footer);
+        assertThat(text(footer)).contains("describe with AI");
+
+        tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
+        assertThat(tab.isTextInputActive()).isTrue();
+        assertThat(TuiTestHelper.renderToString(tab, 160, 
40)).contains("Describe the chaos you want in chaosdemo");
+        // Enter on an empty request does nothing
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        assertThat(asked).isEmpty();
+        for (char ch : "shipping is slow".toCharArray()) {
+            tab.handleKeyEvent(KeyEvent.ofChar(ch, KeyModifiers.NONE));
+        }
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        // asked at once, as the user typed it and pressed Enter; nothing goes 
to the integration
+        assertThat(asked).singleElement().asString().startsWith("now: Turn my 
request into camel-chaos rules")
+                .contains("My request: shipping is slow");
+        assertThat(tab.control().isOpen()).isFalse();
+    }
+
+    @Test
+    void withoutTheAiThereIsNoDescribe() {
+        ctx.askAiCallback = null;
+        ChaosTab tab = new ChaosTab(ctx);
+        TuiTestHelper.renderToString(tab, 160, 40);
+        tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
+        assertThat(tab.control().isOpen()).isFalse();
+        List<Span> footer = new ArrayList<>();
+        tab.renderFooter(footer);
+        assertThat(text(footer)).doesNotContain("describe");
+    }
+
     @Test
     void theQuestionHasTheRuleItsEffectAndTheRoutesItHit() {
         String question = ChaosAiPrompt.of(info, info.chaos.rule("r1"));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRequestEvalTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRequestEvalTest.java
new file mode 100644
index 000000000000..c27e1f4dd4ed
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRequestEvalTest.java
@@ -0,0 +1,564 @@
+/*
+ * 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.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.EndpointAware;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.chaos.ChaosManager;
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.DefaultProducer;
+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.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * The eval set of the "describe the chaos you want" input of the Chaos screen 
(CAMEL-26721, S2-5 of CAMEL-26607): loose
+ * requests ({@code chaos-eval/requests.json}), each sent to a local model as 
the AI panel sends it (its system prompt,
+ * the core tools and the chaos tools, and the question of {@link 
ChaosAiPrompt#ofRequest}). The check tool is answered
+ * by camel-chaos itself, over routes shaped like the chaos demo; the rules 
the model adds or changes are recorded, as
+ * if the user confirmed them, and scored against what the request means. 
Skipped unless {@code CAMEL_TUI_CHAOS_EVAL}
+ * names the model, for example:
+ *
+ * <pre>
+ * CAMEL_TUI_CHAOS_EVAL=qwen3.6:35b-mlx mvn test -Dtest=ChaosRequestEvalTest
+ * </pre>
+ *
+ * {@code CAMEL_TUI_CHAOS_EVAL_RUNS} runs each request more than once (default 
1), {@code CAMEL_TUI_CHAOS_EVAL_ONLY}
+ * runs only the requests whose id contains it, {@code 
CAMEL_TUI_CHAOS_EVAL_VERBOSE=true} prints each tool call, and
+ * {@code OLLAMA_HOST} overrides the server (default {@code 
http://localhost:11434}). It prints a line per request and a
+ * summary, and does not fail on a wrong answer: it measures a model.
+ */
+class ChaosRequestEvalTest {
+
+    // the most turns of a conversation before it counts as stuck
+    private static final int MAX_TURNS = 8;
+    private static final String YES = "yes, add them";
+    // the known rules of the catalog, as the Chaos screen loads them
+    private static final ChaosRecipes RECIPES = ChaosRecipes.load();
+    private static final boolean VERBOSE = 
Boolean.parseBoolean(System.getenv("CAMEL_TUI_CHAOS_EVAL_VERBOSE"));
+
+    /** A rule the model added (change null), or the new rule of a rule it 
changed. */
+    record Proposed(String change, String rule) {
+    }
+
+    /** How one request went. */
+    record Outcome(String id, String failure, List<Proposed> proposed, int 
toolCalls, int checks, double seconds,
+            long tokens) {
+
+        boolean passed() {
+            return failure == null;
+        }
+    }
+
+    // ---- the scoring, which always runs ----
+
+    @Test
+    void theScoringReadsTheRulesAsTheParserDoes() throws Exception {
+        JsonObject spain = (JsonObject) Jsoner.deserialize("""
+                {"target": "route=orders", "fault": ["error"], "rate": null, 
"when": ["country", "ES"]}""");
+        assertThat(mismatch(spain, new Proposed(null, "route=orders error 
when=\"${header.country} == 'ES'\""))).isNull();
+        assertThat(mismatch(spain, new Proposed(null, "route=orders error 
when='${header.country} == \"ES\"'"))).isNull();
+        assertThat(mismatch(spain, new Proposed(null, "route=orders error 
rate=0.05 when=\"${header.country} == 'ES'\"")))
+                .isEqualTo("rate=0.05, expected none");
+        assertThat(mismatch(spain, new Proposed(null, "route=orders 
error"))).isEqualTo("no when= with country, ES");
+        // rate=1 fires at every match, as no rate does
+        assertThat(mismatch(spain, new Proposed(null, "route=orders error 
rate=1 when=\"${header.country} == 'ES'\"")))
+                .isNull();
+        assertThat(mismatch(spain, new Proposed(null, "route=shipping error 
when=\"${header.country} == 'ES'\"")))
+                .isEqualTo("target route=shipping, expected route=orders");
+
+        JsonObject slow = (JsonObject) Jsoner.deserialize("""
+                {"target": "endpoint=http.*pay.*", "fault": ["delay"], 
"duration": "(1s|1000ms)\\\\.\\\\.(3s|3000ms)"}""");
+        assertThat(mismatch(slow, new Proposed(null, 
"endpoint=http://localhost:8765/pay delay duration=1s..3s"))).isNull();
+        assertThat(mismatch(slow, new Proposed(null, "endpoint=http:*pay* 
error duration=1s..3s")))
+                .isEqualTo("fault error, expected [delay]");
+
+        JsonObject change = (JsonObject) Jsoner.deserialize("""
+                {"change": "r1", "target": "endpoint=http.*pay.*", "fault": 
["error"], "rate": "0\\\\.10?"}""");
+        assertThat(mismatch(change, new Proposed("r1", "endpoint=http:*pay* 
error rate=0.1"))).isNull();
+        assertThat(mismatch(change, new Proposed(null, "endpoint=http:*pay* 
error rate=0.1")))
+                .isEqualTo("added, expected a change of r1");
+
+        // every expected rule matches one rule, and no rule is left over
+        List<JsonObject> two = List.of(spain, (JsonObject) 
Jsoner.deserialize("""
+                {"target": "route=orders", "fault": ["error"], "rate": 
"0\\\\.05"}"""));
+        List<Proposed> both = List.of(new Proposed(null, "route=orders error 
rate=0.05"),
+                new Proposed(null, "route=orders error 
when=\"${header.country} == 'ES'\""));
+        assertThat(score(two, both)).isNull();
+        assertThat(score(two, both.subList(0, 1))).startsWith("2 rules 
expected, 1 added or changed");
+        assertThat(score(two, List.of())).isEqualTo("no rule added or 
changed");
+    }
+
+    /** Why the rule is not the expected one, or null when it is. */
+    static String mismatch(JsonObject expect, Proposed proposed) {
+        String change = expect.getString("change");
+        if (change != null && !change.equals(proposed.change())) {
+            return proposed.change() == null ? "added, expected a change of " 
+ change : "changed " + proposed.change();
+        }
+        if (change == null && proposed.change() != null) {
+            return "changed " + proposed.change() + ", expected an added rule";
+        }
+        Map<String, String> rule = parse(proposed.rule());
+        String target = rule.get("@target");
+        if (target == null || !Pattern.matches(expect.getString("target"), 
target)) {
+            return "target " + target + ", expected " + 
expect.getString("target");
+        }
+        List<Object> faults = expect.getCollection("fault");
+        if (!faults.contains(rule.get("@fault"))) {
+            return "fault " + rule.get("@fault") + ", expected " + faults;
+        }
+        for (Map.Entry<String, Object> e : expect.entrySet()) {
+            String key = e.getKey();
+            if (List.of("change", "target", "fault", "when").contains(key)) {
+                continue;
+            }
+            String value = rule.get(key);
+            if ("rate".equals(key) && value != null && List.of("1", "1.0", 
"every:1").contains(value)) {
+                // fires at every match, as without a rate
+                value = null;
+            }
+            if (e.getValue() == null) {
+                if (value != null) {
+                    return key + "=" + value + ", expected none";
+                }
+            } else if (value == null || !Pattern.matches((String) 
e.getValue(), value)) {
+                return key + "=" + value + ", expected " + e.getValue();
+            }
+        }
+        List<Object> when = expect.getCollection("when");
+        if (when != null) {
+            String condition = rule.get("when");
+            if (condition == null || !when.stream().allMatch(w -> 
condition.contains((String) w))) {
+                return (condition == null ? "no when=" : "when=" + condition + 
" without") + " with "
+                       + String.join(", ", 
when.stream().map(String::valueOf).toList());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Whether the rules are the expected ones: each expected rule matches its 
own rule, and none is left over. Null
+     * when they are, else why not.
+     */
+    static String score(List<JsonObject> expected, List<Proposed> proposed) {
+        if (proposed.isEmpty()) {
+            return "no rule added or changed";
+        }
+        if (expected.size() != proposed.size()) {
+            return expected.size() + " rules expected, " + proposed.size() + " 
added or changed: "
+                   + proposed.stream().map(Proposed::rule).toList();
+        }
+        List<Proposed> left = new ArrayList<>(proposed);
+        for (JsonObject expect : expected) {
+            String why = null;
+            Proposed match = null;
+            for (Proposed p : left) {
+                String m = mismatch(expect, p);
+                if (m == null) {
+                    match = p;
+                    break;
+                }
+                why = why == null ? m + " in [" + p.rule() + "]" : why;
+            }
+            if (match == null) {
+                return why;
+            }
+            left.remove(match);
+        }
+        return null;
+    }
+
+    /** The tokens of a rule (quote aware, the quotes left out), by name; the 
target as @target, the fault as @fault. */
+    static Map<String, String> parse(String rule) {
+        Map<String, String> answer = new LinkedHashMap<>();
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        List<String> tokens = new ArrayList<>();
+        for (int i = 0; i < rule.length(); i++) {
+            char ch = rule.charAt(i);
+            if (quote != 0) {
+                if (ch == '\\' && i + 1 < rule.length()) {
+                    current.append(rule.charAt(++i));
+                } else if (ch == quote) {
+                    quote = 0;
+                } else {
+                    current.append(ch);
+                }
+            } else if (ch == '"' || ch == '\'') {
+                quote = ch;
+            } else if (ch == ' ' || ch == '\t') {
+                if (!current.isEmpty()) {
+                    tokens.add(current.toString());
+                    current.setLength(0);
+                }
+            } else {
+                current.append(ch);
+            }
+        }
+        if (!current.isEmpty()) {
+            tokens.add(current.toString());
+        }
+        for (String token : tokens) {
+            int eq = token.indexOf('=');
+            if (eq < 0) {
+                answer.put("@fault", token);
+                continue;
+            }
+            String key = token.substring(0, eq);
+            if (List.of("endpoint", "component", "route", 
"step").contains(key)) {
+                answer.put("@target", token);
+            } else {
+                answer.put(key, token.substring(eq + 1));
+            }
+        }
+        return answer;
+    }
+
+    // ---- the eval, against a local model ----
+
+    @Test
+    void evalAgainstALocalModel() throws Exception {
+        String model = System.getenv("CAMEL_TUI_CHAOS_EVAL");
+        assumeTrue(model != null && !model.isBlank(), "set 
CAMEL_TUI_CHAOS_EVAL=<model> to run the eval");
+        String host = System.getenv("OLLAMA_HOST");
+        String url = host != null && !host.isBlank() ? host : 
"http://localhost:11434";;
+        url = url.startsWith("http") ? url : "http://"; + url;
+        int runs = 
Integer.parseInt(System.getenv().getOrDefault("CAMEL_TUI_CHAOS_EVAL_RUNS", 
"1"));
+        String only = System.getenv("CAMEL_TUI_CHAOS_EVAL_ONLY");
+
+        JsonObject set;
+        try (InputStream is = 
getClass().getResourceAsStream("/chaos-eval/requests.json")) {
+            set = (JsonObject) Jsoner.deserialize(new 
String(is.readAllBytes(), StandardCharsets.UTF_8));
+        }
+        HttpClient http = 
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+        try (CamelContext context = chaosDemo()) {
+            ChaosManager chaos = ChaosManager.get(context);
+
+            AiPanelToolGroupsTest.FakeApp app = new 
AiPanelToolGroupsTest.FakeApp();
+            app.features = AppFeatures.fromStatus(new 
JsonObject(Map.of("chaos", Map.of("enabled", true))));
+            AiPanel panel = 
AiPanelToolGroupsTest.panel(AiPanel.TOOL_MODE_CORE, app);
+            panel.refreshToolGroupsForTesting();
+            String system = panel.systemPromptForTesting();
+            JsonArray tools = wireTools(panel.toolDefinitionsForTesting());
+
+            System.out.println("Chaos request eval with " + model + " at " + 
url + ", " + tools.size() + " tools, "
+                               + runs + " run(s)");
+            List<Outcome> outcomes = new ArrayList<>();
+            for (Object o : set.getCollection("requests")) {
+                JsonObject request = (JsonObject) o;
+                String id = request.getString("id");
+                if (only != null && !only.isBlank() && !id.contains(only)) {
+                    continue;
+                }
+                List<Object> expect = request.getCollection("expect");
+                List<JsonObject> expected = 
expect.stream().map(JsonObject.class::cast).toList();
+                // the rules the integration has when the request is made: 
only those of the request
+                chaos.reset();
+                JsonArray existing = new JsonArray();
+                List<Object> rules = request.getCollection("existingRules");
+                for (Object r : rules != null ? rules : List.of()) {
+                    String rule = ((JsonObject) r).getString("rule");
+                    String ruleId = chaos.addRule(rule);
+                    existing.add(new JsonObject(Map.of("id", ruleId, 
"version", 1, "state", "active", "rule", rule)));
+                }
+                IntegrationInfo info = integration(context, existing);
+                for (int run = 0; run < runs; run++) {
+                    Outcome outcome = converse(http, url, model, system, 
tools, chaos, info, id,
+                            request.getString("request"), expected);
+                    outcomes.add(outcome);
+                    System.out.println(line(outcome));
+                }
+            }
+            long passed = outcomes.stream().filter(Outcome::passed).count();
+            System.out.printf(Locale.ROOT, "%d of %d passed (%.0f%%), %.1fs 
and %d tool calls on average%n", passed,
+                    outcomes.size(), 100.0 * passed / Math.max(1, 
outcomes.size()),
+                    
outcomes.stream().mapToDouble(Outcome::seconds).average().orElse(0),
+                    
Math.round(outcomes.stream().mapToInt(Outcome::toolCalls).average().orElse(0)));
+            assertThat(outcomes).isNotEmpty();
+        }
+    }
+
+    private static String line(Outcome o) {
+        return String.format(Locale.ROOT, "%-4s %-24s %5.1fs %2d calls %2d 
checks %6d tok  %s",
+                o.passed() ? "PASS" : "FAIL", o.id(), o.seconds(), 
o.toolCalls(), o.checks(), o.tokens(),
+                o.passed()
+                        ? o.proposed().stream().map(p -> (p.change() != null ? 
p.change() + " -> " : "") + p.rule()).toList()
+                        : o.failure());
+    }
+
+    /** One request, as a conversation: the tool calls are answered until the 
model adds or changes rules. */
+    private static Outcome converse(
+            HttpClient http, String url, String model, String system, 
JsonArray tools, ChaosManager chaos,
+            IntegrationInfo info, String id, String request, List<JsonObject> 
expected)
+            throws Exception {
+        JsonArray messages = new JsonArray();
+        messages.add(message("system", system));
+        messages.add(message("user",
+                "[Monitoring chaosdemo (PID 4711)]\n" + 
ChaosAiPrompt.ofRequest(info, request, RECIPES)));
+        List<Proposed> proposed = new ArrayList<>();
+        int calls = 0;
+        int checks = 0;
+        long tokens = 0;
+        boolean saidYes = false;
+        long start = System.nanoTime();
+        for (int turn = 0; turn < MAX_TURNS && proposed.isEmpty(); turn++) {
+            JsonObject reply = chat(http, url, model, messages, tools);
+            tokens += reply.getLongOrDefault("prompt_eval_count", 0L) + 
reply.getLongOrDefault("eval_count", 0L);
+            JsonObject msg = (JsonObject) reply.get("message");
+            messages.add(msg);
+            List<Object> toolCalls = msg.getCollection("tool_calls");
+            if (toolCalls == null || toolCalls.isEmpty()) {
+                if (saidYes) {
+                    break;
+                }
+                // it asks before adding, as in the live try: say yes once
+                messages.add(message("user", YES));
+                saidYes = true;
+                continue;
+            }
+            for (Object tc : toolCalls) {
+                JsonObject function = (JsonObject) ((JsonObject) 
tc).get("function");
+                String name = function.getString("name");
+                Object rawArgs = function.get("arguments");
+                JsonObject args = rawArgs instanceof JsonObject jo
+                        ? jo : (JsonObject) 
Jsoner.deserialize(String.valueOf(rawArgs));
+                calls++;
+                String result;
+                switch (name) {
+                    case TuiToolGroups.CHAOS_CHECK_TOOL -> {
+                        checks++;
+                        result = check(chaos, args.getString("rule"));
+                    }
+                    case TuiToolGroups.CHAOS_ADD_TOOL -> {
+                        for (String rule : splitRules(args.getString("rule"))) 
{
+                            proposed.add(new Proposed(null, rule));
+                        }
+                        result = "{\"added\": true, \"note\": \"the user 
confirmed\"}";
+                    }
+                    case TuiToolGroups.CHAOS_CHANGE_TOOL -> {
+                        proposed.add(new Proposed(args.getString("id"), 
args.getString("rule")));
+                        result = "{\"changed\": true, \"note\": \"the user 
confirmed\"}";
+                    }
+                    default -> result = "{\"error\": \"" + name + " is not 
available here: use the chaos tools\"}";
+                }
+                if (VERBOSE) {
+                    System.out.println("     " + id + " " + name + " " + 
args.toJson() + " -> "
+                                       + (result.length() > 300 ? 
result.substring(0, 300) + "..." : result));
+                }
+                JsonObject toolMessage = message("tool", result);
+                toolMessage.put("tool_name", name);
+                messages.add(toolMessage);
+            }
+        }
+        double seconds = (System.nanoTime() - start) / 1e9;
+        return new Outcome(id, score(expected, proposed), proposed, calls, 
checks, seconds, tokens);
+    }
+
+    private static String check(ChaosManager chaos, String rule) {
+        if (rule == null || rule.isBlank()) {
+            return "{\"error\": \"rule is required\"}";
+        }
+        try {
+            return Jsoner.serialize(chaos.checkRules(rule));
+        } catch (IllegalArgumentException e) {
+            return "{\"error\": " + Jsoner.serialize(e.getMessage()) + "}";
+        }
+    }
+
+    // the rules of a text with several separated by ; (a ; in quotes does not 
separate)
+    private static List<String> splitRules(String text) {
+        List<String> answer = new ArrayList<>();
+        if (text == null) {
+            return answer;
+        }
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        for (char ch : text.toCharArray()) {
+            if (quote != 0) {
+                quote = ch == quote ? 0 : quote;
+                current.append(ch);
+            } else if (ch == ';') {
+                answer.add(current.toString().strip());
+                current.setLength(0);
+            } else {
+                quote = ch == '"' || ch == '\'' ? ch : 0;
+                current.append(ch);
+            }
+        }
+        answer.add(current.toString().strip());
+        answer.removeIf(String::isEmpty);
+        return answer;
+    }
+
+    private static JsonObject chat(HttpClient http, String url, String model, 
JsonArray messages, JsonArray tools)
+            throws Exception {
+        JsonObject options = new JsonObject();
+        options.put("temperature", 0.3);
+        options.put("num_ctx", 32768);
+        JsonObject request = new JsonObject();
+        request.put("model", model);
+        request.put("messages", messages);
+        request.put("tools", tools);
+        request.put("stream", false);
+        request.put("think", false);
+        request.put("keep_alive", "30m");
+        request.put("options", options);
+        HttpResponse<String> response = http.send(
+                HttpRequest.newBuilder(URI.create(url + "/api/chat"))
+                        .timeout(Duration.ofMinutes(10))
+                        .header("Content-Type", "application/json")
+                        
.POST(HttpRequest.BodyPublishers.ofString(request.toJson(), 
StandardCharsets.UTF_8))
+                        .build(),
+                HttpResponse.BodyHandlers.ofString());
+        return (JsonObject) Jsoner.deserialize(response.body());
+    }
+
+    private static JsonObject message(String role, String content) {
+        JsonObject m = new JsonObject();
+        m.put("role", role);
+        m.put("content", content);
+        return m;
+    }
+
+    private static JsonArray wireTools(List<LlmClient.ToolDef> defs) {
+        JsonArray tools = new JsonArray();
+        for (LlmClient.ToolDef def : defs) {
+            JsonObject function = new JsonObject();
+            function.put("name", def.name());
+            function.put("description", def.description());
+            function.put("parameters", def.parameters());
+            JsonObject tool = new JsonObject();
+            tool.put("type", "function");
+            tool.put("function", function);
+            tools.add(tool);
+        }
+        return tools;
+    }
+
+    // ---- the routes of the chaos demo, with camel-chaos on ----
+
+    private static CamelContext chaosDemo() throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        Properties props = new Properties();
+        props.put("camel.chaos.enabled", "true");
+        props.put("camel.chaos.seed", "42");
+        context.getPropertiesComponent().setOverrideProperties(props);
+        // the HTTP services, which the routes call but never reach: the 
routes do not run (the timer waits)
+        context.addComponent("http", new SilentComponent());
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
errorHandler(deadLetterChannel("seda:dlq").maximumRedeliveries(2).redeliveryDelay(200));
+                from("timer:orders?period=500&delay=3600000").routeId("orders")
+                        .setHeader("country", constant("DK")).id("pickCountry")
+                        .to("direct:inventory").id("callInventory")
+                        .to("http://localhost:8765/stock";).id("callStock")
+                        .to("direct:payment").id("callPayment")
+                        .to("seda:shipping").id("toShipping");
+                from("direct:inventory").routeId("inventory")
+                        .setHeader("reserved", constant(true)).id("reserve")
+                        .log("reserved ${body}").id("logReserved");
+                from("direct:payment").routeId("payment")
+                        .to("http://localhost:8765/pay";).id("callPay");
+                from("seda:shipping").routeId("shipping")
+                        .log("shipping ${header.orderId}").id("logShipping");
+                
from("file:target/chaos-eval/inbox?delay=3600000&initialDelay=3600000").routeId("returns")
+                        .log("return ${header.CamelFileName}").id("logReturn");
+                from("seda:dlq").routeId("dlq").log("DLQ ${body}");
+            }
+        });
+        context.start();
+        return context;
+    }
+
+    /** The integration as the TUI sees it: its routes, the endpoints each 
sends to, and its chaos rules. */
+    private static IntegrationInfo integration(CamelContext context, JsonArray 
rules) {
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "4711";
+        info.name = "chaosdemo";
+        for (Route route : context.getRoutes()) {
+            RouteInfo r = new RouteInfo();
+            r.routeId = route.getRouteId();
+            r.from = route.getEndpoint().getEndpointUri();
+            info.routes.add(r);
+            for (Processor p : route.filter("*")) {
+                if (p instanceof EndpointAware aware && aware.getEndpoint() != 
null) {
+                    EndpointInfo e = new EndpointInfo();
+                    e.uri = aware.getEndpoint().getEndpointUri();
+                    e.direction = "out";
+                    e.routeId = route.getRouteId();
+                    info.endpoints.add(e);
+                }
+            }
+        }
+        info.chaos = ChaosInfo.parse(new JsonObject(Map.of("enabled", true, 
"rules", rules)));
+        return info;
+    }
+
+    /** An HTTP component that answers nothing: the eval only needs its 
endpoints. */
+    static final class SilentComponent extends DefaultComponent {
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new DefaultEndpoint(uri, this) {
+                @Override
+                public Producer createProducer() {
+                    return new DefaultProducer(this) {
+                        @Override
+                        public void process(Exchange exchange) {
+                            // no answer
+                        }
+                    };
+                }
+
+                @Override
+                public Consumer createConsumer(Processor processor) {
+                    throw new UnsupportedOperationException("the eval has no 
HTTP server");
+                }
+            };
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
index 9a3c5573681d..2c7630f4faf5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
@@ -117,6 +117,32 @@ class McpFacadeConfirmTest {
                 .containsExactly("run SQL that is not a SELECT", "update a row 
of orders");
     }
 
+    @Test
+    void addingAndChangingAChaosRuleAskFirstAndCheckingDoesNot() throws 
Exception {
+        AnsweringBridge bridge = new AnsweringBridge(false);
+        TuiToolRegistry registry = new TuiToolRegistry(facade(context(), 
bridge));
+
+        JsonObject added = (JsonObject) 
Jsoner.deserialize(registry.execute("tui_chaos_add",
+                new JsonObject(Map.of("rule", "route=orders error 
rate=0.05"))));
+        JsonObject changed = (JsonObject) 
Jsoner.deserialize(registry.execute("tui_chaos_change",
+                new JsonObject(Map.of("id", "r1", "rule", "route=orders error 
rate=0.1"))));
+
+        // not confirmed: nothing went to the integration
+        assertThat(added.getString("status")).isEqualTo("rejected");
+        assertThat(changed.getString("status")).isEqualTo("rejected");
+        assertThat(bridge.calls).extracting(ToolConfirms.Call::target)
+                .containsExactly("route=orders error rate=0.05", "route=orders 
error rate=0.1");
+        assertThat(registry.execute("tui_chaos_add", new 
JsonObject(Map.of()))).isEqualTo("Error: rule is required");
+
+        // the check only reads: it is not asked (here no integration answers 
it)
+        try {
+            registry.execute("tui_chaos_check", new JsonObject(Map.of("rule", 
"route=orders error")));
+        } catch (RuntimeException e) {
+            // no integration with pid 1 runs in the test
+        }
+        assertThat(bridge.calls).hasSize(2);
+    }
+
     @Test
     void groovyAsksFirstThroughTheTool() throws Exception {
         AnsweringBridge bridge = new AnsweringBridge(false);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index 0b0b7534ec34..fe7d84bb4d54 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
             AiPanel.java 3200
             AiPanel.runAgentLoop 159
             AiUsageStats.render 166
-            CamelMonitor.java 2624
+            CamelMonitor.java 2632
             DiagramSupport.java 2360
             DiagramSupport.loadHighlightedNativeDiagramInBackground 200
             EndpointsTab.renderContent 170
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/resources/chaos-eval/requests.json
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/resources/chaos-eval/requests.json
new file mode 100644
index 000000000000..a88508cca117
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/resources/chaos-eval/requests.json
@@ -0,0 +1,277 @@
+{
+  "description": "Loose requests for chaos rules, against the routes of the 
chaos demo (orders calls inventory, the stock service over http, payment and 
shipping; payment calls the pay service over http; returns polls a folder; 
seda:dlq is the dead letter channel). Each expected rule matches one rule the 
model adds: target is a regular expression of the target token, the other keys 
a regular expression of the option or parameter value (null: it must be 
absent), the fault one of a list, a [...]
+  "requests": [
+    {
+      "id": "fail-or-spain",
+      "request": "the order route should fail 5% of the time, or when the 
country is spain",
+      "expect": [
+        {
+          "target": "route=orders",
+          "fault": [
+            "error"
+          ],
+          "rate": "0\\.05"
+        },
+        {
+          "target": "route=orders",
+          "fault": [
+            "error"
+          ],
+          "rate": null,
+          "when": [
+            "country",
+            "ES"
+          ]
+        }
+      ]
+    },
+    {
+      "id": "stock-503",
+      "request": "make the stock service return 503 for a fifth of the calls",
+      "expect": [
+        {
+          "target": "endpoint=http.*stock.*",
+          "fault": [
+            "error"
+          ],
+          "status": "503",
+          "rate": "0\\.20?"
+        }
+      ]
+    },
+    {
+      "id": "payment-slow",
+      "request": "the payment service is slow, between 1 and 3 seconds",
+      "expect": [
+        {
+          "target": "endpoint=http.*pay.*",
+          "fault": [
+            "delay"
+          ],
+          "duration": "(1s|1000ms)\\.\\.(3s|3000ms)"
+        }
+      ]
+    },
+    {
+      "id": "payment-timeout",
+      "request": "calls to the payment service time out after 5 seconds",
+      "expect": [
+        {
+          "target": "endpoint=http.*pay.*",
+          "fault": [
+            "error"
+          ],
+          "after": "5s|5000ms|5000",
+          "exception": ".*Timeout.*"
+        }
+      ]
+    },
+    {
+      "id": "lost-shipments",
+      "request": "every 10th shipment is lost",
+      "expect": [
+        {
+          "target": "endpoint=seda:(//)?shipping.*|component=seda",
+          "fault": [
+            "drop"
+          ],
+          "rate": "every:10"
+        }
+      ]
+    },
+    {
+      "id": "duplicate-shipments",
+      "request": "send each shipment twice, to see if shipping copes with 
duplicates",
+      "expect": [
+        {
+          "target": "endpoint=seda:(//)?shipping.*",
+          "fault": [
+            "duplicate"
+          ],
+          "rate": null
+        }
+      ]
+    },
+    {
+      "id": "spain-not-shipped",
+      "request": "orders from Spain should never reach shipping",
+      "expect": [
+        {
+          "target": "endpoint=seda:(//)?shipping.*",
+          "fault": [
+            "drop"
+          ],
+          "rate": null,
+          "when": [
+            "country",
+            "ES"
+          ]
+        }
+      ]
+    },
+    {
+      "id": "returns-stall",
+      "request": "pause the returns route for 30 seconds",
+      "expect": [
+        {
+          "target": "route=returns",
+          "fault": [
+            "stall",
+            "lifecycle"
+          ],
+          "duration": "30s|30000ms"
+        }
+      ]
+    },
+    {
+      "id": "returns-polls",
+      "request": "half of the polls of the returns folder fail",
+      "expect": [
+        {
+          "target": "endpoint=file.*inbox.*|component=file",
+          "fault": [
+            "error"
+          ],
+          "at": "poll",
+          "rate": "0\\.50?"
+        }
+      ]
+    },
+    {
+      "id": "dlq-down",
+      "request": "the dead letter queue is down",
+      "expect": [
+        {
+          "target": "endpoint=seda:(//)?dlq.*",
+          "fault": [
+            "error"
+          ],
+          "at": "dlq"
+        }
+      ]
+    },
+    {
+      "id": "reserve-bug",
+      "request": "the step that reserves the stock in the inventory route 
throws an IllegalStateException now and then, say 10%",
+      "expect": [
+        {
+          "target": "step=reserve",
+          "fault": [
+            "error"
+          ],
+          "exception": "(java\\.lang\\.)?IllegalStateException",
+          "rate": "0\\.10?"
+        }
+      ]
+    },
+    {
+      "id": "orders-http-500",
+      "request": "every http call the orders route makes fails with a 500, for 
the next 2 minutes",
+      "expect": [
+        {
+          "target": "endpoint=http.*|component=http",
+          "fault": [
+            "error"
+          ],
+          "status": "500",
+          "until": "2m|120s",
+          "in": "orders"
+        }
+      ]
+    },
+    {
+      "id": "truncate-shipping",
+      "request": "cut the messages sent to shipping in half, a quarter of the 
time",
+      "expect": [
+        {
+          "target": "endpoint=seda:(//)?shipping.*",
+          "fault": [
+            "corrupt"
+          ],
+          "mode": "truncate",
+          "rate": "0\\.25"
+        }
+      ]
+    },
+    {
+      "id": "stock-outage",
+      "request": "the stock service is down for 10 seconds every minute",
+      "expect": [
+        {
+          "target": "endpoint=http.*stock.*",
+          "fault": [
+            "error"
+          ],
+          "rate": "burst:(10s|10000ms)/(1m|60s)"
+        }
+      ]
+    },
+    {
+      "id": "warm-up",
+      "request": "after a 30 second warm-up, fail 5% of the orders",
+      "expect": [
+        {
+          "target": "route=orders",
+          "fault": [
+            "error"
+          ],
+          "rate": "0\\.05",
+          "from": "\\+?(30s|30000ms)"
+        }
+      ]
+    },
+    {
+      "id": "fewer-payment-failures",
+      "request": "the payment failures are too many, make it 10%",
+      "expect": [
+        {
+          "change": "r1",
+          "target": "endpoint=http.*pay.*",
+          "fault": [
+            "error"
+          ],
+          "status": "503",
+          "rate": "0\\.10?"
+        }
+      ],
+      "existingRules": [
+        {
+          "id": "r1",
+          "rule": "endpoint=http://localhost:8765/pay error status=503 
rate=0.5"
+        }
+      ]
+    },
+    {
+      "id": "spain-payments",
+      "request": "payments of Spanish orders fail with a 503",
+      "expect": [
+        {
+          "target": "endpoint=http.*pay.*",
+          "fault": [
+            "error"
+          ],
+          "status": "503",
+          "rate": null,
+          "when": [
+            "country",
+            "ES"
+          ]
+        }
+      ]
+    },
+    {
+      "id": "payment-route-slow",
+      "request": "every step of the payment route is 200 milliseconds slower",
+      "expect": [
+        {
+          "target": "step=\\*",
+          "fault": [
+            "delay"
+          ],
+          "duration": "200ms",
+          "in": "payment"
+        }
+      ]
+    }
+  ]
+}

Reply via email to