Repository: langchain4j/langchain4j (2026-07-11T00:15:28Z)

Issue:
[BUG] Non-numeric timeout_seconds argument throws raw NumberFormatException in run_shell_command tool
**Describe the bug**

In `langchain4j-experimental-skills-shell`, `RunShellCommandToolExecutor.resolveTimeout(Map)` calls `Integer.valueOf(timeoutSeconds.toString())` without a guard. `timeout_seconds` is registered as a **string** schema property (`ShellSkills`, via `addStringProperty`, described as "The command timeout in seconds. Default: 30 seconds"), so an LLM can legitimately return non-integer strings such as `"30 seconds"`, `"1.5"`, or `""`. That raises a raw `NumberFormatException` which propagates out of `executeWithContext`.

This bypasses the tool's argument-error contract: every other argument error in this class is routed through `throwException(...)`, which honors the `throwToolArgumentsExceptions` flag (`ToolExecutionException` by default so the message returns to the LLM, or `ToolArgumentsException` when configured).

**Log and Stack trace**

`java.lang.NumberFormatException: For input string: "30 seconds"` thrown from `RunShellCommandToolExecutor.resolveTimeout`.

**To Reproduce**

Invoke the `run_shell_command` tool with arguments `{"command":"echo x","timeout_seconds":"abc"}`.

**Expected behavior**

A non-numeric `timeout_seconds` should follow the same argument-error path as missing/unparseable arguments: `ToolExecutionException` by default, or `ToolArgumentsException` when `throwToolArgumentsExceptions(true)`.

**Please complete the following information:**
- LangChain4j version: 1.18.0-beta28 (main)
- LLM(s) used: N/A
- Java version: 17
- Spring Boot version (if applicable): N/A

**Additional context**

N/A




Discussion:

Patch:
diff --git a/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutor.java b/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutor.java
index e23adf45bb7..1fb4bdcb52a 100644
--- a/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutor.java
+++ b/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutor.java
@@ -119,7 +119,15 @@ Integer resolveTimeout(Map<String, Object> arguments) {
         if (timeoutSeconds instanceof Integer i) {
             return i;
         }
-        return Integer.valueOf(timeoutSeconds.toString());
+        try {
+            return Integer.valueOf(timeoutSeconds.toString());
+        } catch (NumberFormatException e) {
+            throwException(
+                    "Invalid value for tool argument '%s': '%s'"
+                            .formatted(config.timeoutSecondsParameterName, timeoutSeconds),
+                    e);
+            return null; // unreachable
+        }
     }
 
     private String formatStdOut(String stdOut) {
diff --git a/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/ShellSkills.java b/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/ShellSkills.java
index ce63d69d667..fbaa3518fa1 100644
--- a/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/ShellSkills.java
+++ b/experimental/langchain4j-experimental-skills-shell/src/main/java/dev/langchain4j/skills/shell/ShellSkills.java
@@ -118,7 +118,7 @@ private ToolProvider createToolProvider(Builder builder) {
                 .description(rsc.description)
                 .parameters(JsonObjectSchema.builder()
                         .addStringProperty(rsc.commandParameterName, rsc.commandParameterDescription)
-                        .addStringProperty(rsc.timeoutSecondsParameterName, rsc.timeoutSecondsParameterDescription)
+                        .addIntegerProperty(rsc.timeoutSecondsParameterName, rsc.timeoutSecondsParameterDescription)
                         .required(rsc.commandParameterName)
                         .build())
                 .addMetadata(METADATA_SEARCH_BEHAVIOR, ALWAYS_VISIBLE)


Test patch:
diff --git a/experimental/langchain4j-experimental-skills-shell/src/test/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutorTest.java b/experimental/langchain4j-experimental-skills-shell/src/test/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutorTest.java
index b7e396e926a..aa8932cb2bc 100644
--- a/experimental/langchain4j-experimental-skills-shell/src/test/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutorTest.java
+++ b/experimental/langchain4j-experimental-skills-shell/src/test/java/dev/langchain4j/skills/shell/RunShellCommandToolExecutorTest.java
@@ -46,6 +46,26 @@ void should_throw_ToolArgumentsException_when_command_argument_value_is_null() {
                 .hasMessageContaining("Missing required tool argument");
     }
 
+    @Test
+    void should_throw_ToolExecutionException_when_timeout_seconds_is_not_a_number() {
+        RunShellCommandToolExecutor executor = executor(false);
+
+        assertThatThrownBy(() -> executor.executeWithContext(
+                        requestWithRawArguments("{\"command\": \"echo x\", \"timeout_seconds\": \"abc\"}"), null))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("Invalid value for tool argument");
+    }
+
+    @Test
+    void should_throw_ToolArgumentsException_when_timeout_seconds_is_not_a_number() {
+        RunShellCommandToolExecutor executor = executor(true);
+
+        assertThatThrownBy(() -> executor.executeWithContext(
+                        requestWithRawArguments("{\"command\": \"echo x\", \"timeout_seconds\": \"abc\"}"), null))
+                .isInstanceOf(ToolArgumentsException.class)
+                .hasMessageContaining("Invalid value for tool argument");
+    }
+
     @Test
     @DisabledOnOs(OS.WINDOWS)
     void should_not_truncate_stdout_on_success_when_within_limit_on_unix() {