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

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

commit 120ed0fa0e77d5dba4f0c70cd257c67cd3ee6a95
Author: Luigi De Masi <[email protected]>
AuthorDate: Thu Sep 24 17:32:00 2026 +0200

    CAMEL-24977: Document semantic retry and contextual action validation
    
    Clarify the Semantic Evaluation title and description while retaining the
    existing artifact and language names. Document bounded retry and contextual
    action validation through existing Camel hooks, with regression coverage for
    budget exhaustion, permission rejection, uncertainty and provider failures.
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Luigi De Masi <[email protected]>
---
 .../camel/catalog/docs/semantic-language.adoc      |  98 +++++++-
 .../apache/camel/catalog/languages/semantic.json   |   4 +-
 components/camel-ai/camel-semantic/pom.xml         |   4 +-
 .../apache/camel/language/semantic/semantic.json   |   4 +-
 .../services/org/apache/camel/language.properties  |   4 +-
 .../src/main/docs/semantic-language.adoc           |  98 +++++++-
 .../camel/language/semantic/SemanticLanguage.java  |   3 +-
 .../semantic/SemanticRetryAndValidationTest.java   | 272 +++++++++++++++++++++
 docs/components/modules/languages/nav.adoc         |   2 +-
 9 files changed, 471 insertions(+), 18 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 1e50cb70781e..2b71a6ae2387 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -1,8 +1,8 @@
-= Semantic Language
-:doctitle: Semantic
+= Semantic Evaluation Language
+:doctitle: Semantic Evaluation
 :shortname: semantic
 :artifactid: camel-semantic
-:description: Evaluate named semantic questions through a provider adapter
+:description: Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters
 :since: 4.23
 :supportlevel: Preview
 :tabs-sync-option:
@@ -136,7 +136,7 @@ Use `language("semantic", "ref:name")` wherever an 
expression or boolean predica
 
 [cols="1,3"]
 |===
-|EIP |Usage
+|EIP / integration point |Usage
 |Choice |Store a category with Set Property, then compare that property in 
ordinary when predicates.
 |Filter |Use a boolean question as the filter predicate.
 |Validate |Use a boolean question; false follows normal validation failure 
handling.
@@ -148,6 +148,8 @@ Use `language("semantic", "ref:name")` wherever an 
expression or boolean predica
 |Enrich |Map a category to a configured resource URI and use an ordinary 
aggregation strategy.
 |Loop |Reevaluate a boolean question on updated state, with an explicit 
iteration or time budget.
 |Sort |Score each item once, store the scores, then sort using a deterministic 
comparator.
+|On Exception / retryWhile |Evaluate whether another attempt is worthwhile, 
with an explicit retry budget checked before inference.
+|Contextual action validation |Use Validate after ordinary permission checks 
and before executing the action.
 |===
 
 Destination mappings belong to trusted route configuration; provider output 
should select
@@ -234,6 +236,94 @@ its score. Use `.sort(body(), 
Comparator.comparingDouble(ScoredItem::score))` wi
 score. The comparator must not call the provider: sorting can compare an item 
multiple times
 and in an implementation-dependent order.
 
+=== Bounded semantic retry
+
+A boolean question can supply the 
xref:manual::exception-advanced.adoc[retryWhile predicate]
+for an exception clause. Ask whether another attempt is worthwhile using the 
current failure
+and selected request context. Restrict this to operations that are safe to 
retry.
+
+IMPORTANT: `retryWhile` replaces the `maximumRedeliveries` decision. Check the 
retry budget
+inside the predicate, before calling the provider. A provider that always 
returns true must
+not cause unlimited retries or evaluations.
+
+Declare a boolean question named `retryable` with `state: 
$\{exchangeProperty.retryState}`.
+In this example, the request body is a string. `onExceptionOccurred` prepares 
the state before
+the retry predicate runs; `onRedelivery` runs later and is too late for this 
purpose.
+Include only the failure details needed by the question, excluding credentials 
and sensitive data.
+
+[source,java]
+----
+Predicate retryable = 
context.resolveLanguage("semantic").createPredicate("ref:retryable");
+
+onException(IOException.class)
+    .onExceptionOccurred(exchange -> {
+        Exception failure = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class);
+        exchange.setProperty("retryState", Map.of(
+            "request", exchange.getMessage().getBody(String.class),
+            "failureType", failure.getClass().getSimpleName(),
+            "attempt", 
exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, Integer.class)));
+    })
+    .retryWhile(exchange ->
+        exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, 0, 
Integer.class) <= 3
+            && retryable.matches(exchange))
+    .redeliveryDelay(1000)
+    .handled(true)
+    .to("direct:escalate");
+----
+
+The counter starts at one when deciding the first redelivery. This permits at 
most three
+redeliveries after the initial attempt. Each failure refreshes the selected 
state; redelivery
+restarts at the failed processor, not at the beginning of the route. A 
negative decision or
+an exhausted budget sends the message to `direct:escalate` through normal 
exception handling.
+
+Evaluation errors are distinct from a negative decision. If the retry 
predicate throws,
+Camel reports the evaluation failure on the exchange; it does not 
automatically execute
+the exception clause's escalation route. Arrange caller or supervising-route 
handling for
+that failure. Bound provider timeouts as well as the retry count.
+
+=== Contextual action validation
+
+Use a semantic question for an additional check such as "Does this proposed 
action serve
+the approved task?" after normal identity, permission and tenant checks have 
succeeded.
+The semantic decision must not grant permissions that those checks denied.
+
+For example, declare this question alongside routes:
+
+[source,yaml]
+----
+- semantic:
+    question:
+      withinScope:
+        type: boolean
+        instructions: Does the proposed action serve the approved task?
+        state: ${body}
+        threshold: 0.8
+        uncertainty: 0.05
+        uncertaintyPolicy: fail
+----
+
+The selected state should contain the approved task and the proposed action. 
Obtain the
+approved task from trusted application state; do not let the proposed action 
redefine it.
+In this example, `direct:checkPermissions` performs the application's existing 
authorization
+and rejects unauthorized requests before semantic evaluation:
+
+[source,java]
+----
+from("direct:action")
+    .to("direct:checkPermissions")
+    .validate().language("semantic", "ref:withinScope")
+    .to("direct:performAction");
+----
+
+A negative decision raises normal validation failure. With this question's 
`fail` policy,
+an uncertain decision raises an evaluation error. Timeouts and malformed 
responses also
+fail, and none of these outcomes should execute the protected action. Failure 
handling may
+reject the request or send it for human review; do not use `continued(true)` 
to resume at
+the action. The example's threshold is a decision policy, not an accuracy 
guarantee.
+
+This pattern uses Validate and existing authorization services. It does not 
add a semantic
+`AuthorizationPolicy` implementation or replace a component's internal 
guardrail interfaces.
+
 == Implementing an adapter
 
 Implement `org.apache.camel.semantic.SemanticAdapter`. Advertise the 
implementation in
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
index a55839e37138..4e05df3493c2 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/semantic.json
@@ -2,8 +2,8 @@
   "language": {
     "kind": "language",
     "name": "semantic",
-    "title": "Semantic",
-    "description": "Evaluate named semantic questions through a provider 
adapter",
+    "title": "Semantic Evaluation",
+    "description": "Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters",
     "deprecated": false,
     "firstVersion": "4.23.0",
     "label": "language,ai",
diff --git a/components/camel-ai/camel-semantic/pom.xml 
b/components/camel-ai/camel-semantic/pom.xml
index 0bd826ff70b6..7c3e05e0eafe 100644
--- a/components/camel-ai/camel-semantic/pom.xml
+++ b/components/camel-ai/camel-semantic/pom.xml
@@ -26,8 +26,8 @@
         <version>4.23.0-SNAPSHOT</version>
     </parent>
     <artifactId>camel-semantic</artifactId>
-    <name>Camel :: AI :: Semantic</name>
-    <description>Provider-independent semantic evaluation language and adapter 
SPI</description>
+    <name>Camel :: AI :: Semantic Evaluation</name>
+    <description>Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters</description>
     <dependencies>
         <dependency>
             <groupId>org.apache.camel</groupId>
diff --git 
a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
 
b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
index a55839e37138..4e05df3493c2 100644
--- 
a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
+++ 
b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json
@@ -2,8 +2,8 @@
   "language": {
     "kind": "language",
     "name": "semantic",
-    "title": "Semantic",
-    "description": "Evaluate named semantic questions through a provider 
adapter",
+    "title": "Semantic Evaluation",
+    "description": "Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters",
     "deprecated": false,
     "firstVersion": "4.23.0",
     "label": "language,ai",
diff --git 
a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
 
b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
index 263824973361..78cea1e65e68 100644
--- 
a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
+++ 
b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties
@@ -3,5 +3,5 @@ languages=semantic
 groupId=org.apache.camel
 artifactId=camel-semantic
 version=4.23.0-SNAPSHOT
-projectName=Camel :: AI :: Semantic
-projectDescription=Provider-independent semantic evaluation language and 
adapter SPI
+projectName=Camel :: AI :: Semantic Evaluation
+projectDescription=Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters
diff --git 
a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc 
b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 1e50cb70781e..2b71a6ae2387 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -1,8 +1,8 @@
-= Semantic Language
-:doctitle: Semantic
+= Semantic Evaluation Language
+:doctitle: Semantic Evaluation
 :shortname: semantic
 :artifactid: camel-semantic
-:description: Evaluate named semantic questions through a provider adapter
+:description: Evaluate named questions about message content to produce 
boolean decisions, categories and scores through provider adapters
 :since: 4.23
 :supportlevel: Preview
 :tabs-sync-option:
@@ -136,7 +136,7 @@ Use `language("semantic", "ref:name")` wherever an 
expression or boolean predica
 
 [cols="1,3"]
 |===
-|EIP |Usage
+|EIP / integration point |Usage
 |Choice |Store a category with Set Property, then compare that property in 
ordinary when predicates.
 |Filter |Use a boolean question as the filter predicate.
 |Validate |Use a boolean question; false follows normal validation failure 
handling.
@@ -148,6 +148,8 @@ Use `language("semantic", "ref:name")` wherever an 
expression or boolean predica
 |Enrich |Map a category to a configured resource URI and use an ordinary 
aggregation strategy.
 |Loop |Reevaluate a boolean question on updated state, with an explicit 
iteration or time budget.
 |Sort |Score each item once, store the scores, then sort using a deterministic 
comparator.
+|On Exception / retryWhile |Evaluate whether another attempt is worthwhile, 
with an explicit retry budget checked before inference.
+|Contextual action validation |Use Validate after ordinary permission checks 
and before executing the action.
 |===
 
 Destination mappings belong to trusted route configuration; provider output 
should select
@@ -234,6 +236,94 @@ its score. Use `.sort(body(), 
Comparator.comparingDouble(ScoredItem::score))` wi
 score. The comparator must not call the provider: sorting can compare an item 
multiple times
 and in an implementation-dependent order.
 
+=== Bounded semantic retry
+
+A boolean question can supply the 
xref:manual::exception-advanced.adoc[retryWhile predicate]
+for an exception clause. Ask whether another attempt is worthwhile using the 
current failure
+and selected request context. Restrict this to operations that are safe to 
retry.
+
+IMPORTANT: `retryWhile` replaces the `maximumRedeliveries` decision. Check the 
retry budget
+inside the predicate, before calling the provider. A provider that always 
returns true must
+not cause unlimited retries or evaluations.
+
+Declare a boolean question named `retryable` with `state: 
$\{exchangeProperty.retryState}`.
+In this example, the request body is a string. `onExceptionOccurred` prepares 
the state before
+the retry predicate runs; `onRedelivery` runs later and is too late for this 
purpose.
+Include only the failure details needed by the question, excluding credentials 
and sensitive data.
+
+[source,java]
+----
+Predicate retryable = 
context.resolveLanguage("semantic").createPredicate("ref:retryable");
+
+onException(IOException.class)
+    .onExceptionOccurred(exchange -> {
+        Exception failure = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class);
+        exchange.setProperty("retryState", Map.of(
+            "request", exchange.getMessage().getBody(String.class),
+            "failureType", failure.getClass().getSimpleName(),
+            "attempt", 
exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, Integer.class)));
+    })
+    .retryWhile(exchange ->
+        exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, 0, 
Integer.class) <= 3
+            && retryable.matches(exchange))
+    .redeliveryDelay(1000)
+    .handled(true)
+    .to("direct:escalate");
+----
+
+The counter starts at one when deciding the first redelivery. This permits at 
most three
+redeliveries after the initial attempt. Each failure refreshes the selected 
state; redelivery
+restarts at the failed processor, not at the beginning of the route. A 
negative decision or
+an exhausted budget sends the message to `direct:escalate` through normal 
exception handling.
+
+Evaluation errors are distinct from a negative decision. If the retry 
predicate throws,
+Camel reports the evaluation failure on the exchange; it does not 
automatically execute
+the exception clause's escalation route. Arrange caller or supervising-route 
handling for
+that failure. Bound provider timeouts as well as the retry count.
+
+=== Contextual action validation
+
+Use a semantic question for an additional check such as "Does this proposed 
action serve
+the approved task?" after normal identity, permission and tenant checks have 
succeeded.
+The semantic decision must not grant permissions that those checks denied.
+
+For example, declare this question alongside routes:
+
+[source,yaml]
+----
+- semantic:
+    question:
+      withinScope:
+        type: boolean
+        instructions: Does the proposed action serve the approved task?
+        state: ${body}
+        threshold: 0.8
+        uncertainty: 0.05
+        uncertaintyPolicy: fail
+----
+
+The selected state should contain the approved task and the proposed action. 
Obtain the
+approved task from trusted application state; do not let the proposed action 
redefine it.
+In this example, `direct:checkPermissions` performs the application's existing 
authorization
+and rejects unauthorized requests before semantic evaluation:
+
+[source,java]
+----
+from("direct:action")
+    .to("direct:checkPermissions")
+    .validate().language("semantic", "ref:withinScope")
+    .to("direct:performAction");
+----
+
+A negative decision raises normal validation failure. With this question's 
`fail` policy,
+an uncertain decision raises an evaluation error. Timeouts and malformed 
responses also
+fail, and none of these outcomes should execute the protected action. Failure 
handling may
+reject the request or send it for human review; do not use `continued(true)` 
to resume at
+the action. The example's threshold is a decision policy, not an accuracy 
guarantee.
+
+This pattern uses Validate and existing authorization services. It does not 
add a semantic
+`AuthorizationPolicy` implementation or replace a component's internal 
guardrail interfaces.
+
 == Implementing an adapter
 
 Implement `org.apache.camel.semantic.SemanticAdapter`. Advertise the 
implementation in
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index b88dbee4bdbd..89257cb6ee4d 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -47,7 +47,8 @@ import org.apache.camel.util.IOHelper;
 
 /** Evaluates a named, provider-independent question against selected message 
state. */
 @Language(value = "semantic", modelName = "language")
-@Metadata(title = "Semantic", description = "Evaluate named semantic questions 
through a provider adapter",
+@Metadata(title = "Semantic Evaluation",
+          description = "Evaluate named questions about message content to 
produce boolean decisions, categories and scores through provider adapters",
           label = "language,ai", firstVersion = "4.23.0")
 public class SemanticLanguage extends LanguageSupport {
     public static final String RESULT = "CamelSemanticResult";
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
new file mode 100644
index 000000000000..96f922aaa3d6
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
@@ -0,0 +1,272 @@
+/*
+ * 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.semantic;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.Exchange;
+import org.apache.camel.Predicate;
+import org.apache.camel.ValidationException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SemanticRetryAndValidationTest extends CamelTestSupport {
+    private final AtomicInteger attempts = new AtomicInteger();
+    private final AtomicInteger entries = new AtomicInteger();
+    private final List<Object> evaluatedStates = new CopyOnWriteArrayList<>();
+    private volatile int failures = Integer.MAX_VALUE;
+    private volatile double probability = 0.9;
+    private volatile boolean malformedResult;
+    private volatile Exception evaluationFailure;
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                context.getRegistry().bind("evaluator", new SemanticAdapter() {
+                    @Override
+                    public void validate(SemanticQuestion question) {
+                    }
+
+                    @Override
+                    public SemanticResult evaluate(SemanticQuestion question, 
Object state) throws Exception {
+                        evaluatedStates.add(state);
+                        if (evaluationFailure != null) {
+                            throw evaluationFailure;
+                        }
+                        return malformedResult
+                                ? new SemanticResult("unexpected category", 
null, null, null, null)
+                                : new SemanticResult(null, probability, null, 
null, null);
+                    }
+                });
+                SemanticLanguage language = (SemanticLanguage) 
context.resolveLanguage("semantic");
+                language.setAdapter("evaluator");
+                SemanticQuestions.get(context).replace("test", Map.of(
+                        "retryable", question("Is another attempt 
worthwhile?", "${exchangeProperty.retryState}"),
+                        "withinScope", question("Does the proposed action 
serve the approved task?", "${body}")));
+                Predicate retryable = 
language.createPredicate("ref:retryable");
+
+                
errorHandler(defaultErrorHandler().maximumRedeliveries(0).logExhausted(false));
+                onException(IOException.class)
+                        .onExceptionOccurred(exchange -> {
+                            Exception failure = 
exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
+                            exchange.setProperty("retryState", Map.of(
+                                    "request", 
exchange.getMessage().getBody(String.class),
+                                    "failure", failure.getMessage(),
+                                    "attempt", 
exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, Integer.class)));
+                        })
+                        .retryWhile(
+                                exchange -> 
exchange.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, 0, Integer.class) 
<= 3
+                                        && retryable.matches(exchange))
+                        .redeliveryDelay(0)
+                        .handled(true)
+                        .to("mock:escalated");
+
+                from("direct:retry")
+                        .process(exchange -> entries.incrementAndGet())
+                        .process(exchange -> {
+                            int attempt = attempts.incrementAndGet();
+                            if (attempt <= failures) {
+                                throw new IOException("Failure " + attempt);
+                            }
+                        })
+                        .to("mock:completed");
+
+                from("direct:action")
+                        .to("direct:checkPermissions")
+                        .validate().language("semantic", "ref:withinScope")
+                        .to("mock:performed");
+                from("direct:checkPermissions").process(exchange -> {
+                    // Stand-in for the application's trusted authorization 
service, not a caller-supplied header.
+                    if 
(!Boolean.TRUE.equals(exchange.getProperty("permissionGranted", 
Boolean.class))) {
+                        throw new CamelAuthorizationException("Permission 
denied", exchange);
+                    }
+                });
+            }
+        };
+    }
+
+    private static SemanticQuestion question(String instructions, String 
state) {
+        return new SemanticQuestion(
+                SemanticQuestion.Type.BOOLEAN, instructions, state, null, null,
+                0.8, 0.05, SemanticQuestion.UncertaintyPolicy.FAIL);
+    }
+
+    @Test
+    void approvedRetryResumesAtFailedProcessorAndPreservesBody() throws 
Exception {
+        failures = 1;
+        getMockEndpoint("mock:completed").expectedBodiesReceived("operation");
+        getMockEndpoint("mock:escalated").expectedMessageCount(0);
+
+        Exchange exchange = template.request("direct:retry", e -> 
e.getMessage().setBody("operation"));
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("operation");
+        assertThat(attempts).hasValue(2);
+        assertThat(entries).hasValue(1);
+        assertThat(evaluatedStates).containsExactly(retryState(1));
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void negativeRetryDecisionEscalatesWithoutAnotherAttempt() throws 
Exception {
+        probability = 0.1;
+        getMockEndpoint("mock:completed").expectedMessageCount(0);
+        getMockEndpoint("mock:escalated").expectedBodiesReceived("operation");
+
+        Exchange exchange = template.request("direct:retry", e -> 
e.getMessage().setBody("operation"));
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class))
+                .isInstanceOf(IOException.class).hasMessage("Failure 1");
+        assertThat(attempts).hasValue(1);
+        assertThat(evaluatedStates).containsExactly(retryState(1));
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void alwaysApprovedRetryStopsAtBudgetBeforeAnotherEvaluation() throws 
Exception {
+        getMockEndpoint("mock:completed").expectedMessageCount(0);
+        getMockEndpoint("mock:escalated").expectedBodiesReceived("operation");
+
+        Exchange exchange = template.request("direct:retry", e -> 
e.getMessage().setBody("operation"));
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class))
+                .isInstanceOf(IOException.class).hasMessage("Failure 4");
+        assertThat(attempts).hasValue(4);
+        assertThat(entries).hasValue(1);
+        assertThat(evaluatedStates).containsExactly(retryState(1), 
retryState(2), retryState(3));
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "timeout", "malformed", "uncertain" })
+    void retryEvaluationFailurePropagatesWithoutRetryOrNormalEscalation(String 
failure) throws Exception {
+        failEvaluation(failure);
+        getMockEndpoint("mock:completed").expectedMessageCount(0);
+        getMockEndpoint("mock:escalated").expectedMessageCount(0);
+
+        Exchange exchange = template.request("direct:retry", e -> 
e.getMessage().setBody("operation"));
+
+        assertEvaluationFailure(exchange, failure);
+        assertThat(exchange.getMessage().getBody()).isEqualTo("operation");
+        assertThat(exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class)).isInstanceOf(IOException.class);
+        assertThat(attempts).hasValue(1);
+        assertThat(evaluatedStates).containsExactly(retryState(1));
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void approvedContextExecutesActionAndPreservesState() throws Exception {
+        
getMockEndpoint("mock:performed").expectedBodiesReceived(actionState());
+
+        Exchange exchange = requestAction(true);
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo(actionState());
+        assertThat(evaluatedStates).containsExactly(actionState());
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void deniedPermissionPreventsEvaluationAndAction() throws Exception {
+        getMockEndpoint("mock:performed").expectedMessageCount(0);
+
+        Exchange exchange = requestAction(false);
+
+        
assertThat(exchange.getException()).isInstanceOf(CamelAuthorizationException.class);
+        assertThat(evaluatedStates).isEmpty();
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    void negativeContextDecisionPreventsActionDespitePermission() throws 
Exception {
+        probability = 0.1;
+        getMockEndpoint("mock:performed").expectedMessageCount(0);
+
+        Exchange exchange = requestAction(true);
+
+        
assertThat(exchange.getException()).isInstanceOf(ValidationException.class);
+        assertThat(evaluatedStates).containsExactly(actionState());
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "timeout", "malformed", "uncertain" })
+    void contextEvaluationFailurePreventsActionDespitePermission(String 
failure) throws Exception {
+        failEvaluation(failure);
+        getMockEndpoint("mock:performed").expectedMessageCount(0);
+
+        Exchange exchange = requestAction(true);
+
+        assertEvaluationFailure(exchange, failure);
+        assertThat(exchange.getMessage().getBody()).isEqualTo(actionState());
+        assertThat(evaluatedStates).containsExactly(actionState());
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    private Exchange requestAction(boolean permissionGranted) {
+        return template.request("direct:action", exchange -> {
+            exchange.setProperty("permissionGranted", permissionGranted);
+            exchange.getMessage().setBody(actionState());
+        });
+    }
+
+    private static Map<String, Object> retryState(int attempt) {
+        return Map.of("request", "operation", "failure", "Failure " + attempt, 
"attempt", attempt);
+    }
+
+    private static Map<String, String> actionState() {
+        return Map.of("approvedTask", "Send the customer their order status", 
"proposedAction", "Email the order status");
+    }
+
+    private void failEvaluation(String failure) {
+        switch (failure) {
+            case "timeout" -> evaluationFailure = new 
TimeoutException("Evaluation timed out");
+            case "malformed" -> malformedResult = true;
+            case "uncertain" -> probability = 0.8;
+            default -> throw new IllegalArgumentException(failure);
+        }
+    }
+
+    private static void assertEvaluationFailure(Exchange exchange, String 
failure) {
+        switch (failure) {
+            case "timeout" -> 
assertThat(exchange.getException(TimeoutException.class)).hasMessage("Evaluation
 timed out");
+            case "malformed" -> 
assertThat(exchange.getException(IllegalArgumentException.class))
+                    .hasMessage("Semantic result does not support the question 
and its decision policy");
+            case "uncertain" -> 
assertThat(exchange.getException(IllegalStateException.class))
+                    .hasMessage("Semantic boolean decision is uncertain");
+            default -> throw new IllegalArgumentException(failure);
+        }
+        assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
+    }
+}
diff --git a/docs/components/modules/languages/nav.adoc 
b/docs/components/modules/languages/nav.adoc
index 940325e249d6..970ce0154225 100644
--- a/docs/components/modules/languages/nav.adoc
+++ b/docs/components/modules/languages/nav.adoc
@@ -21,7 +21,7 @@
 ** xref:python3-language.adoc[Python 3]
 ** xref:quickjs-language.adoc[QuickJS]
 ** xref:ref-language.adoc[Ref]
-** xref:semantic-language.adoc[Semantic]
+** xref:semantic-language.adoc[Semantic Evaluation]
 ** xref:simple-language.adoc[Simple]
 ** xref:simple-advanced.adoc[Simple - Advanced Features]
 ** xref:simple-functions.adoc[Simple - Built-in Functions]

Reply via email to