Add timeout support with a hierarchical configuration (skill, model, global) and... master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 18 Jan 2026 21:35:28 +0000 (23:35 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 18 Jan 2026 21:35:28 +0000 (23:35 +0200)
doc/index.org
src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/Task.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcess.java
src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java
src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Model.java
src/main/java/eu/svjatoslav/alyverkko_cli/configuration/SkillConfig.java

index 795e9e9..982fc89 100644 (file)
@@ -711,6 +711,42 @@ multi-document summarization all require sufficient context
 size. Always verify your model's actual supported context - exceeding
 it causes unpredictable or significantly degraded model output.
 
+*** Timeout
+
+/Timeout/ is a parameter that specifies the maximum time (in
+milliseconds) that the AI is allowed to run for a task. If this time
+is exceeded, the process is terminated, and the response is marked
+with "TERMINATED BY TIMEOUT".
+
+The timeout parameter can be set at three levels:
+1. *Skill-specific*: Defined in the skill YAML file.
+2. *Model-specific*: Defined in the model configuration.
+3. *Global default*: Set in the main configuration file.
+
+The priority hierarchy is: *skill* > *model* > *global default*.
+
+For example, to set a 5-minute timeout (300,000 milliseconds) for a specific skill, add:
+: timeout_millis: 300000
+in the skill's YAML file.
+
+In the model configuration:
+#+begin_src yaml
+models:
+  - alias: "mistral"
+    timeout_millis: 600000
+    # ... other parameters
+#+end_src
+
+In the main configuration:
+#+begin_src yaml
+default_timeout_millis: 120000
+#+end_src
+
+Setting a timeout of 0 means no timeout.
+
+This feature helps prevent stuck AI processes and ensures predictable
+task completion times.
+
 *** Parameter Precedence Hierarchy
 :PROPERTIES:
 :ID:       456dd42e-a474-4464-a14e-384c68713537
index 2e80c53..1cd2865 100644 (file)
@@ -128,5 +128,30 @@ public class Task {
         else return configuration.getDefaultMinP();
     }
 
+    /**
+     * Calculates the effective timeout in milliseconds using the following hierarchy:
+     * <ol>
+     *   <li>Skill-specific timeout (highest priority)</li>
+     *   <li>Model-specific timeout</li>
+     *   <li>Global default timeout (lowest priority)</li>
+     * </ol>
+     *
+     * @return the effective timeout in milliseconds, or null if no timeout is configured.
+     */
+    public Long getEffectiveTimeoutMillis() {
+        // Skill-specific has the highest priority
+        if (skill != null && skill.getTimeoutMillis() != null) {
+            return skill.getTimeoutMillis();
+        }
+
+        // Model-specific next
+        if (model.getTimeoutMillis() != null) {
+            return model.getTimeoutMillis();
+        }
+
+        // Global default as fallback
+        return configuration.getDefaultTimeoutMillis();
+    }
+
 
 }
index ad6f68a..32f18b9 100644 (file)
@@ -5,6 +5,7 @@ import eu.svjatoslav.alyverkko_cli.Utils;
 import java.io.*;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
 
 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
 import static java.lang.String.join;
@@ -63,6 +64,7 @@ public class TaskProcess {
         return task.systemPrompt.replace("<TASK-FILE>", task.userPrompt);
     }
 
+
     /**
      * Runs the AI query by constructing the prompt, writing it to a temp file,
      * invoking llama.cpp, collecting output, and performing any final cleanup.
@@ -94,8 +96,24 @@ public class TaskProcess {
             StringBuilder result = new StringBuilder();
             Thread outputThread = handleResultThread(process, result);
 
-            // Wait for the process to finish
-            process.waitFor();
+            // Get effective timeout value
+            Long timeoutMillis = task.getEffectiveTimeoutMillis();
+            boolean isTimedOut = false;
+
+            if (timeoutMillis != null && timeoutMillis > 0) {
+                try {
+                    if (!process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) {
+                        process.destroyForcibly();
+                        isTimedOut = true;
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    process.destroyForcibly();
+                    isTimedOut = true;
+                }
+            } else {
+                process.waitFor();
+            }
 
             // Wait for the output thread to finish reading
             outputThread.join();
@@ -104,7 +122,12 @@ public class TaskProcess {
             task.endTimeMillis = System.currentTimeMillis();
 
             // Clean up the AI response: remove partial prompt text, end-of-text marker, etc.
-            return cleanupAiResponse(result.toString());
+            String cleanedResponse = cleanupAiResponse(result.toString());
+            if (isTimedOut) {
+                cleanedResponse += "\nTERMINATED BY TIMEOUT";
+            }
+
+            return cleanedResponse;
         } finally {
             deleteTemporaryFile();
         }
index 1e425f9..018109f 100644 (file)
@@ -97,6 +97,14 @@ public class Configuration {
     @JsonProperty("skills_directory")
     private File skillsDirectory;
 
+    /**
+     * The default timeout in milliseconds for AI processing tasks. A value of 0 or null means no timeout.
+     * This serves as the lowest-priority fallback when no skill-specific or model-specific timeout is set.
+     */
+    @JsonProperty("default_timeout_millis")
+    private Long defaultTimeoutMillis;
+
+
     /**
      * The list of models defined in this configuration.
      */
index 26f4406..33eadfd 100644 (file)
@@ -67,6 +67,13 @@ public class Model {
     @JsonProperty("final_answer_indicator")
     private String finalAnswerIndicator;
 
+    /**
+     * Maximum time in milliseconds allowed for AI processing for this model. If null, no timeout is set for this model.
+     * This value overrides the global default timeout but is overridden by skill-specific timeouts.
+     */
+    @JsonProperty("timeout_millis")
+    private Long timeoutMillis;
+
     /**
      * <p>Prints the model's metadata to standard output in a consistent format. This includes the model's alias,
      * filesystem path, and context token capacity. The output format is designed to be both human-readable and
index ec242e0..f260938 100644 (file)
@@ -41,4 +41,11 @@ public class SkillConfig {
     @JsonProperty("model_alias")
     private String modelAlias;
 
+    /**
+     * Maximum time in milliseconds allowed for AI processing when this skill is used. If null, no timeout is set for this skill.
+     * This is the highest-priority timeout value in the hierarchy, overriding model-specific and global default timeouts.
+     */
+    @JsonProperty("timeout_millis")
+    private Long timeoutMillis;
+
 }
\ No newline at end of file