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

davsclaus pushed a commit to branch feature/CAMEL-25513-write-reload-output
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b15ba9eb570873a79434c2483cfc5f5be52e514d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 10 07:51:50 2026 +0200

    CAMEL-25513: camel-jbang-mcp - a write reports the reload and the first 
output of the integration running from its directory
    
    camel_write_file and camel_edit_file reported the reload of a dev-mode 
integration only for a selected
    process. Through camel-jbang-mcp the agent passes the directory and no 
name, so no write reported one
    (0 of 68 step traces in a ladder run), and the agent wrote blind: in 
transform-xslt the stylesheet
    produced empty attributes the model never saw.
    
    - with no integration selected, the write tools find the one running in dev 
mode from the directory
      (IntegrationLauncher.devModeFrom, as camel_run finds one) and await its 
reload
    - after a reload the answer carries the first lines the routes logged (and 
any WARN or ERROR) within
      3s, in reload.output and in the message
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  3 ++
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 40 +++++++++++---
 .../core/commands/ai/IntegrationLauncher.java      | 14 +++++
 .../dsl/jbang/core/commands/ai/ReloadOutcome.java  | 63 ++++++++++++++++++++++
 .../jbang/core/commands/ai/ReloadOutcomeTest.java  | 27 ++++++++++
 5 files changed, 141 insertions(+), 6 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 3caabf9a0d22..e39157d531ff 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -347,6 +347,9 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   plain file name in the directory is accepted. Nobody is asked before the 
write: the MCP client (Claude Code,
   Cursor and the others ask before a tool that is not read-only runs) is where 
the human sits, and the
   `read-only` access level of the security layer hides the tool altogether.
+  When an integration runs in dev mode from the directory (as `camel_run` 
starts it), the answer says how it
+  reloaded the file and what its routes logged right after, so the agent sees 
the result of the change, such as
+  an empty value its route now produces, without a call to `camel_get_log`.
 
 | `camel_edit_file`
 | Changes a file by replacing one snippet: the lines to find (they must name 
one place; other indentation is fine)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 453aed7c17bc..cedd14b6a98e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -607,6 +607,8 @@ public final class AuthoringTools {
 
     /** How long a write waits for the running integration's reload record 
before answering without it. */
     static final long RELOAD_WAIT_MILLIS = 8000;
+    /** How long a write waits after the reload for the first output of the 
routes (CAMEL-25513). */
+    static final long OUTPUT_WAIT_MILLIS = 3000;
 
     /**
      * Replaces one snippet of a file and writes the result through {@link 
#writeFile}, so a change to an existing file
@@ -1093,11 +1095,25 @@ public final class AuthoringTools {
         // than the ones before the write
         String processName = null;
         String sinceKey = null;
-        boolean watch = ctx.hasProcess() && 
SourceValidator.isValidatableFile(file);
+        long watchPid = -1;
+        if (SourceValidator.isValidatableFile(file)) {
+            if (ctx.hasProcess()) {
+                watchPid = ctx.pid();
+                RuntimeHelper.ProcessInfo p = 
RuntimeHelper.findProcess(Long.toString(ctx.pid()));
+                processName = p != null ? p.name() : null;
+            } else {
+                // no integration selected, as through camel-jbang-mcp where 
the agent passes the directory: the one
+                // running from it in dev mode reloads the file (CAMEL-25513)
+                RuntimeHelper.ProcessInfo p = 
IntegrationLauncher.devModeFrom(dir);
+                if (p != null) {
+                    watchPid = p.pid();
+                    processName = p.name();
+                }
+            }
+        }
+        boolean watch = watchPid >= 0;
         if (watch) {
-            RuntimeHelper.ProcessInfo p = 
RuntimeHelper.findProcess(Long.toString(ctx.pid()));
-            processName = p != null ? p.name() : null;
-            sinceKey = 
ReloadOutcome.latestReloadKey(ReloadOutcome.records(ctx.pid(), processName));
+            sinceKey = 
ReloadOutcome.latestReloadKey(ReloadOutcome.records(watchPid, processName));
         }
         try {
             Files.createDirectories(path.getParent());
@@ -1126,11 +1142,23 @@ public final class AuthoringTools {
             }
         }
         if (watch) {
-            JsonObject reload = ReloadOutcome.await(ctx.pid(), processName, 
sinceKey, RELOAD_WAIT_MILLIS);
+            JsonObject reload = ReloadOutcome.await(watchPid, processName, 
sinceKey, RELOAD_WAIT_MILLIS);
             result.put("reload", reload);
             String status = reload.getString("status");
+            String logged = "";
+            if ("reloaded".equals(status)) {
+                // the result of the change, so the agent sees what its route 
now does (CAMEL-25513)
+                JsonArray output = ReloadOutcome.awaitOutput(watchPid, 
processName, OUTPUT_WAIT_MILLIS);
+                reload.put("output", output);
+                logged = output.isEmpty()
+                        ? " It logged nothing within " + OUTPUT_WAIT_MILLIS / 
1000 + "s after the reload (camel_get_log"
+                          + " shows what it logs later)."
+                        : " After the reload it logged: " + output.get(0)
+                          + (output.size() > 1 ? " (and " + (output.size() - 
1) + " more in reload.output)" : "");
+            }
+            final String loggedAfter = logged;
             result.put("message", switch (status) {
-                case "reloaded" -> "The running integration reloaded the 
file.";
+                case "reloaded" -> "The running integration reloaded the 
file." + loggedAfter;
                 case "properties" -> "The running integration reloaded the 
properties.";
                 case "failed" -> "The running integration FAILED to reload the 
file, the route is not running; fix the"
                                  + " content and write again (see 
reload.message).";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index ef81b0aff447..42d02caec526 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -353,6 +353,20 @@ public final class IntegrationLauncher {
     }
 
     /** The integration that runs with this directory as its working 
directory, or null. */
+    /**
+     * The integration that runs in dev mode from the directory (its working 
directory, started with --dev), or null:
+     * the one whose reload a write to the directory is answered with when no 
integration is selected (CAMEL-25513).
+     */
+    static RuntimeHelper.ProcessInfo devModeFrom(Path directory) {
+        RuntimeHelper.ProcessInfo running = runningFrom(directory);
+        if (running == null) {
+            return null;
+        }
+        List<String> args = ProcessHandle.of(running.pid()).flatMap(ph -> 
ph.info().arguments()).map(Arrays::asList)
+                .orElse(null);
+        return args != null && args.contains("--dev") ? running : null;
+    }
+
     private static RuntimeHelper.ProcessInfo runningFrom(Path directory) {
         Path wanted = directory.toAbsolutePath().normalize();
         for (RuntimeHelper.ProcessInfo p : RuntimeHelper.discoverProcesses()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
index 5f5ba2b52a0a..d3759fcc391b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
@@ -16,9 +16,14 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
 
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
 /**
@@ -135,6 +140,64 @@ public final class ReloadOutcome {
         return out;
     }
 
+    /**
+     * A record a route logged: the log EIP's logger is the route's source 
location (xslt.camel.yaml:12), or its route
+     * id when source locations are off, which has no dots, unlike the class 
names of Camel's own loggers.
+     */
+    private static final Pattern ROUTE_LOGGER = 
Pattern.compile("(^|\\.)(yaml|yml|xml|java|groovy):\\d+$");
+    private static final int OUTPUT_LINES = 5;
+    private static final int OUTPUT_CHARS = 400;
+
+    /**
+     * What the routes logged after the newest reload, waiting up to the 
timeout for the first of it: the records of the
+     * routes' log steps, and any WARN or ERROR, newest last, at most five, 
each cut to 400 characters. A write answered
+     * with the result of the change shows the agent at once what its route 
now does (CAMEL-25513): an agent left to
+     * call camel_get_log reads it before the reload ran, or not at all.
+     */
+    public static JsonArray awaitOutput(long pid, String name, long 
timeoutMillis) {
+        StopWatch watch = new StopWatch();
+        JsonArray out = output(records(pid, name));
+        while (out.isEmpty() && watch.taken() < timeoutMillis) {
+            try {
+                Thread.sleep(300);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+            out = output(records(pid, name));
+        }
+        return out;
+    }
+
+    static JsonArray output(List<JsonObject> newestFirst) {
+        List<String> lines = new ArrayList<>();
+        for (JsonObject r : newestFirst) {
+            if (isReload(r)) {
+                break; // the newest reload: what came before it is not the 
result of this write
+            }
+            String level = r.getStringOrDefault("level", "");
+            String logger = r.getStringOrDefault("logger", "");
+            boolean routeLog = ROUTE_LOGGER.matcher(logger).find()
+                    || !logger.isEmpty() && 
Character.isLowerCase(logger.charAt(0));
+            if (!routeLog && !"WARN".equalsIgnoreCase(level) && 
!"ERROR".equalsIgnoreCase(level)) {
+                continue;
+            }
+            String m = r.getStringOrDefault("message", "");
+            String detail = r.getStringOrDefault("detail", "");
+            if (routeLog && !detail.isEmpty()) {
+                // a multi-line body (an XML or JSON document) is part of what 
the route logged
+                m = m + "\n" + detail;
+            }
+            lines.add((routeLog ? "" : level.toUpperCase(Locale.ROOT) + " ")
+                      + (m.length() > OUTPUT_CHARS ? m.substring(0, 
OUTPUT_CHARS) + "..." : m));
+        }
+        // newest first in the log: the first lines the routes logged after 
the reload are at the end
+        Collections.reverse(lines);
+        JsonArray out = new JsonArray();
+        out.addAll(lines.subList(0, Math.min(OUTPUT_LINES, lines.size())));
+        return out;
+    }
+
     @SuppressWarnings("unchecked")
     static List<JsonObject> records(long pid, String name) {
         JsonObject log = LogFileReader.read(pid, name, 40, null, null);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
index 8fc48abb0f91..11343b1776c1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.List;
 
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
@@ -94,4 +95,30 @@ class ReloadOutcomeTest {
         assertEquals("reloaded", out.getString("status"));
         assertEquals("Routes reloaded summary (total:1 started:1)", 
out.getString("message"));
     }
+
+    /** CAMEL-25513: what the routes logged after the reload, the result of 
the write. */
+    @Test
+    void theOutputIsWhatTheRoutesLoggedAfterTheReload() {
+        List<String> xslt = List.of(
+                "2026-10-10 04:44:26.089  INFO 22543 --- [rReloadStrategy] 
e.camel.component.file.FileEndpoint : Endpoint is configured with noop=true so 
forcing endpoint to be idempotent as well",
+                "2026-10-10 04:44:26.097  INFO 22543 --- [rReloadStrategy] 
org.apache.camel.support.RouteWatcherReloadStrategy : Routes reloaded summary 
(total:1 started:1)",
+                "2026-10-10 04:44:26.097  INFO 22543 --- [rReloadStrategy] 
org.apache.camel.support.RouteWatcherReloadStrategy :     Started xslt 
(file://inbox) (source: xslt.camel.yaml:4)",
+                "2026-10-10 04:44:27.167  INFO 22543 --- [ - file://inbox] 
xslt.camel.yaml:12                  : Packing slip: <?xml version=\"1.0\" 
encoding=\"UTF-8\"?><packingSlip orderId=\"\" customer=\"C-482\" 
country=\"\"/>",
+                "2026-10-10 04:44:28.001  WARN 22543 --- [ - file://inbox] 
org.apache.camel.component.file.GenericFileOnCompletion : Rollback file 
strategy: x");
+
+        JsonArray output = ReloadOutcome.output(records(BEFORE, xslt));
+
+        assertEquals(2, output.size(), output.toJson());
+        assertEquals("Packing slip: <?xml version=\"1.0\" 
encoding=\"UTF-8\"?><packingSlip orderId=\"\" customer=\"C-482\""
+                     + " country=\"\"/>",
+                output.get(0));
+        assertTrue(output.get(1).toString().startsWith("WARN Rollback file 
strategy"), output.toJson());
+    }
+
+    @Test
+    void whatWasLoggedBeforeTheNewestReloadIsNotOutput() {
+        assertEquals(List.of("Hello Camel"), 
List.copyOf(ReloadOutcome.output(records(BEFORE))));
+        // after the next reload "Hello Camel" is the result of the write 
before
+        assertEquals(List.of("Hello again"), 
List.copyOf(ReloadOutcome.output(records(BEFORE, RELOADED))));
+    }
 }

Reply via email to