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 8a9b6db119f8 CAMEL-25513: camel-jbang-mcp - a write reports the reload
and the first output of the integration running from its directory (#27647)
8a9b6db119f8 is described below
commit 8a9b6db119f86f0451b9e49a810b6dad9441de62
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 10 08:30:35 2026 +0200
CAMEL-25513: camel-jbang-mcp - a write reports the reload and the first
output of the integration running from its directory (#27647)
* 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
* CAMEL-25513: the javadoc of runningFrom back on runningFrom
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../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..6a3e98982f63 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
@@ -352,6 +352,20 @@ public final class IntegrationLauncher {
: " It is not in dev mode: camel_control restart picks up a
changed or added file.");
}
+ /**
+ * 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;
+ }
+
/** The integration that runs with this directory as its working
directory, or null. */
private static RuntimeHelper.ProcessInfo runningFrom(Path directory) {
Path wanted = directory.toAbsolutePath().normalize();
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))));
+ }
}