clean up local agent
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
logs/
|
||||||
|
.clj-kondo/
|
||||||
|
.lsp/
|
||||||
+136
-27
@@ -2,7 +2,9 @@
|
|||||||
(:require [babashka.http-client :as http]
|
(:require [babashka.http-client :as http]
|
||||||
[cheshire.core :as json]
|
[cheshire.core :as json]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str])
|
||||||
|
(:import [java.time LocalDateTime]
|
||||||
|
[java.time.format DateTimeFormatter]))
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
;; Configuration
|
;; Configuration
|
||||||
@@ -11,12 +13,31 @@
|
|||||||
;; Ollama - local LLM server at http://localhost:11434
|
;; Ollama - local LLM server at http://localhost:11434
|
||||||
(def ollama-host (or (System/getenv "OLLAMA_HOST") "http://localhost:11434"))
|
(def ollama-host (or (System/getenv "OLLAMA_HOST") "http://localhost:11434"))
|
||||||
|
|
||||||
;; Local models with tool use support:
|
;; Local model with tool use support
|
||||||
;; - qwen2.5:1.5b (small, fast)
|
(def model (or (System/getenv "AGENT_MODEL") "qwen3-coder-next"))
|
||||||
;; - qwen2.5:3b (better quality)
|
(def max-tokens 8192)
|
||||||
;; - llama3.2:3b (alternative)
|
|
||||||
(def model (or (System/getenv "AGENT_MODEL") "qwen2.5:1.5b"))
|
;; ============================================================
|
||||||
(def max-tokens 4096)
|
;; Logging
|
||||||
|
;; ============================================================
|
||||||
|
|
||||||
|
(def log-dir
|
||||||
|
(let [home (System/getProperty "user.home")]
|
||||||
|
(.getPath (io/file home ".local" "share" "agent0" "logs"))))
|
||||||
|
(def log-file
|
||||||
|
(let [ts (.format (LocalDateTime/now) (DateTimeFormatter/ofPattern "yyyy-MM-dd_HH-mm-ss"))]
|
||||||
|
(io/file log-dir (str "agent-" ts ".log"))))
|
||||||
|
|
||||||
|
(defn init-log! []
|
||||||
|
(.mkdirs (io/file log-dir))
|
||||||
|
(spit log-file ""))
|
||||||
|
|
||||||
|
(defn log
|
||||||
|
"Write a debug message to the log file."
|
||||||
|
[& args]
|
||||||
|
(let [ts (.format (LocalDateTime/now) (DateTimeFormatter/ofPattern "HH:mm:ss.SSS"))
|
||||||
|
line (str "[" ts "] " (str/join " " args) "\n")]
|
||||||
|
(spit log-file line :append true)))
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
;; Tools Implementation
|
;; Tools Implementation
|
||||||
@@ -30,16 +51,30 @@
|
|||||||
(slurp file)
|
(slurp file)
|
||||||
(str "Error: File not found: " path))))
|
(str "Error: File not found: " path))))
|
||||||
|
|
||||||
|
(defn- format-size
|
||||||
|
"Format file size in human-readable form."
|
||||||
|
[size]
|
||||||
|
(cond
|
||||||
|
(< size 1024) (str size "B")
|
||||||
|
(< size (* 1024 1024)) (format "%.1fK" (/ size 1024.0))
|
||||||
|
:else (format "%.1fM" (/ size (* 1024.0 1024.0)))))
|
||||||
|
|
||||||
(defn list-files
|
(defn list-files
|
||||||
"List files in a directory. Directories have trailing slashes."
|
"List files in a directory with metadata (type, size, permissions)."
|
||||||
[{:keys [path]}]
|
[{:keys [path]}]
|
||||||
(let [dir (io/file (or path "."))]
|
(let [dir (io/file (or path "."))]
|
||||||
(if (.isDirectory dir)
|
(if (.isDirectory dir)
|
||||||
(->> (.listFiles dir)
|
(->> (.listFiles dir)
|
||||||
(map (fn [f]
|
(map (fn [f]
|
||||||
(if (.isDirectory f)
|
(cond
|
||||||
|
(.isDirectory f)
|
||||||
(str (.getName f) "/")
|
(str (.getName f) "/")
|
||||||
(.getName f))))
|
|
||||||
|
(.canExecute f)
|
||||||
|
(str (.getName f) " [executable, " (format-size (.length f)) "]")
|
||||||
|
|
||||||
|
:else
|
||||||
|
(str (.getName f) " [" (format-size (.length f)) "]"))))
|
||||||
(sort)
|
(sort)
|
||||||
(str/join "\n"))
|
(str/join "\n"))
|
||||||
(str "Error: Not a directory: " path))))
|
(str "Error: Not a directory: " path))))
|
||||||
@@ -75,6 +110,18 @@
|
|||||||
(spit file content)
|
(spit file content)
|
||||||
(str "Successfully created " path)))))
|
(str "Successfully created " path)))))
|
||||||
|
|
||||||
|
(defn run-shell-command
|
||||||
|
"Run a shell command and return its output."
|
||||||
|
[{:keys [command]}]
|
||||||
|
(let [proc (-> (ProcessBuilder. ["bash" "-c" command])
|
||||||
|
(.redirectErrorStream true)
|
||||||
|
.start)
|
||||||
|
output (slurp (.getInputStream proc))
|
||||||
|
exit-code (.waitFor proc)]
|
||||||
|
(if (zero? exit-code)
|
||||||
|
output
|
||||||
|
(str output "\n[exit code: " exit-code "]"))))
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
;; Tool Registry (OpenAI format)
|
;; Tool Registry (OpenAI format)
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
@@ -91,7 +138,7 @@
|
|||||||
|
|
||||||
{:type "function"
|
{:type "function"
|
||||||
:function {:name "list_files"
|
:function {:name "list_files"
|
||||||
:description "List files and directories at a given path. Directories have trailing slashes. Use this to explore the project structure."
|
:description "List files and directories at a given path. Directories have trailing /. Executable files are marked [executable, size]. Regular files show [size]. Use this to explore the project structure."
|
||||||
:parameters {:type "object"
|
:parameters {:type "object"
|
||||||
:properties {:path {:type "string"
|
:properties {:path {:type "string"
|
||||||
:description "The relative path to list (defaults to current directory)"}}
|
:description "The relative path to list (defaults to current directory)"}}
|
||||||
@@ -120,7 +167,16 @@
|
|||||||
:content {:type "string"
|
:content {:type "string"
|
||||||
:description "The content to write to the file"}}
|
:description "The content to write to the file"}}
|
||||||
:required ["path" "content"]}}
|
:required ["path" "content"]}}
|
||||||
:impl create-file}])
|
:impl create-file}
|
||||||
|
|
||||||
|
{:type "function"
|
||||||
|
:function {:name "run_shell_command"
|
||||||
|
:description "Run a shell command and return its stdout/stderr output. Use this for tasks like counting lines, searching text, running tests, or any other shell operation."
|
||||||
|
:parameters {:type "object"
|
||||||
|
:properties {:command {:type "string"
|
||||||
|
:description "The shell command to execute"}}
|
||||||
|
:required ["command"]}}
|
||||||
|
:impl run-shell-command}])
|
||||||
|
|
||||||
(def tool-map
|
(def tool-map
|
||||||
(into {} (map (fn [t] [(get-in t [:function :name]) (:impl t)]) tools)))
|
(into {} (map (fn [t] [(get-in t [:function :name]) (:impl t)]) tools)))
|
||||||
@@ -159,6 +215,13 @@
|
|||||||
[message]
|
[message]
|
||||||
(:tool_calls message))
|
(:tool_calls message))
|
||||||
|
|
||||||
|
(defn- truncate
|
||||||
|
"Truncate string to max-len, appending ... if truncated."
|
||||||
|
[s max-len]
|
||||||
|
(if (> (count s) max-len)
|
||||||
|
(str (subs s 0 max-len) "... [truncated, " (count s) " chars total]")
|
||||||
|
s))
|
||||||
|
|
||||||
(defn execute-tool
|
(defn execute-tool
|
||||||
"Execute a tool and return the result message."
|
"Execute a tool and return the result message."
|
||||||
[{:keys [id function]}]
|
[{:keys [id function]}]
|
||||||
@@ -168,31 +231,56 @@
|
|||||||
args (if (string? raw-args)
|
args (if (string? raw-args)
|
||||||
(json/parse-string raw-args true)
|
(json/parse-string raw-args true)
|
||||||
raw-args)]
|
raw-args)]
|
||||||
(println (str " → Tool: " tool-name " " (pr-str args)))
|
(log "→ Tool:" tool-name (pr-str args))
|
||||||
(let [tool-fn (get tool-map tool-name)
|
(let [tool-fn (get tool-map tool-name)
|
||||||
result (if tool-fn
|
result (if tool-fn
|
||||||
(tool-fn args)
|
(tool-fn args)
|
||||||
(str "Error: Unknown tool: " tool-name))]
|
(str "Error: Unknown tool '" tool-name "'. Available tools: "
|
||||||
|
(str/join ", " (keys tool-map))))]
|
||||||
|
(log "← Result:" (truncate (str/replace (str result) #"\n" "\\n") 200))
|
||||||
{:role "tool"
|
{:role "tool"
|
||||||
:tool_call_id id
|
:tool_call_id id
|
||||||
:content result})))
|
:content result})))
|
||||||
|
|
||||||
|
(defn- tool-call-signature
|
||||||
|
"Return a comparable signature for a tool call to detect repetition."
|
||||||
|
[{:keys [function]}]
|
||||||
|
[(:name function) (:arguments function)])
|
||||||
|
|
||||||
|
(defn- detect-stuck-loop
|
||||||
|
"Check if the last N tool calls are identical, indicating a stuck loop.
|
||||||
|
Returns the repeated tool name if stuck, nil otherwise."
|
||||||
|
[tool-calls previous-signatures repeat-threshold]
|
||||||
|
(let [current-sigs (mapv tool-call-signature tool-calls)
|
||||||
|
all-sigs (conj previous-signatures current-sigs)]
|
||||||
|
{:signatures all-sigs
|
||||||
|
:stuck? (when (>= (count all-sigs) repeat-threshold)
|
||||||
|
(let [recent (take-last repeat-threshold all-sigs)]
|
||||||
|
(when (apply = recent)
|
||||||
|
(ffirst (last recent)))))}))
|
||||||
|
|
||||||
(defn run-agent
|
(defn run-agent
|
||||||
"Run the agent loop with the given initial prompt."
|
"Run the agent loop with the given initial prompt."
|
||||||
[prompt]
|
[prompt]
|
||||||
(println (str "Using model: " model " via " ollama-host))
|
(init-log!)
|
||||||
(let [system-prompt "You are a helpful coding assistant. You can read, list, create, and edit files to help the user with their coding tasks. Always explain what you're doing before using tools. Use the tools when needed to complete the task."]
|
(log "Session started | model:" model "| host:" ollama-host)
|
||||||
|
(log "Log file:" (.getPath log-file))
|
||||||
|
(let [system-prompt "You are a helpful coding assistant. You can read, list, create, and edit files to help the user with their coding tasks. Always explain what you're doing before using tools. Use the tools when needed to complete the task."
|
||||||
|
repeat-threshold 3]
|
||||||
(loop [messages [{:role "user" :content prompt}]
|
(loop [messages [{:role "user" :content prompt}]
|
||||||
iteration 0]
|
iteration 0
|
||||||
|
tool-sigs []]
|
||||||
(when (> iteration 50)
|
(when (> iteration 50)
|
||||||
(println "Max iterations reached. Stopping.")
|
(log "Max iterations (50) reached. Stopping.")
|
||||||
|
(println "\nStopping: too many iterations.")
|
||||||
(System/exit 1))
|
(System/exit 1))
|
||||||
|
|
||||||
(println (str "\n[Iteration " iteration "]"))
|
(log "Iteration" iteration "| messages:" (count messages))
|
||||||
|
|
||||||
(let [response (call-llm messages system-prompt)
|
(let [response (call-llm messages system-prompt)
|
||||||
_ (when-let [err (:error response)]
|
_ (when-let [err (:error response)]
|
||||||
(println "API Error:" (or (:message err) (pr-str err)))
|
(log "API error:" (or (:message err) (pr-str err)))
|
||||||
|
(println "Error: could not reach the model.")
|
||||||
(System/exit 1))
|
(System/exit 1))
|
||||||
choice (first (:choices response))
|
choice (first (:choices response))
|
||||||
message (:message choice)
|
message (:message choice)
|
||||||
@@ -200,21 +288,42 @@
|
|||||||
content (:content message)
|
content (:content message)
|
||||||
tool-calls (extract-tool-calls message)]
|
tool-calls (extract-tool-calls message)]
|
||||||
|
|
||||||
|
(log "finish_reason:" finish-reason "| tool_calls:" (count (or tool-calls [])))
|
||||||
|
|
||||||
;; Print assistant's text response
|
;; Print assistant's text response
|
||||||
(when (and content (seq content))
|
(when (and content (seq content))
|
||||||
(println)
|
(println)
|
||||||
(println content))
|
(println content))
|
||||||
|
|
||||||
(if (and (= finish-reason "tool_calls") (seq tool-calls))
|
(if (and (= finish-reason "tool_calls") (seq tool-calls))
|
||||||
;; Execute tools and continue
|
;; Execute tools and continue — but check for stuck loops first
|
||||||
(let [tool-results (mapv execute-tool tool-calls)
|
(let [{:keys [signatures stuck?]} (detect-stuck-loop tool-calls tool-sigs repeat-threshold)]
|
||||||
assistant-msg (select-keys message [:role :content :tool_calls])
|
(if stuck?
|
||||||
new-messages (into (conj messages assistant-msg) tool-results)]
|
(do
|
||||||
(recur new-messages (inc iteration)))
|
(log "Stuck loop detected:" stuck? "repeated" repeat-threshold "times")
|
||||||
|
(println "\nStopping: agent is repeating the same action.")
|
||||||
|
response)
|
||||||
|
(let [_ (doseq [tc tool-calls]
|
||||||
|
(let [tname (get-in tc [:function :name])
|
||||||
|
raw-args (get-in tc [:function :arguments])
|
||||||
|
args (if (string? raw-args)
|
||||||
|
(json/parse-string raw-args true)
|
||||||
|
raw-args)]
|
||||||
|
(println (str " " (case tname
|
||||||
|
"read_file" (str "Reading " (:path args))
|
||||||
|
"list_files" (str "Listing " (or (:path args) "."))
|
||||||
|
"edit_file" (str "Editing " (:path args))
|
||||||
|
"create_file" (str "Creating " (:path args))
|
||||||
|
"run_shell_command" (str "Running `" (:command args) "`")
|
||||||
|
(str "Calling " tname))))))
|
||||||
|
tool-results (mapv execute-tool tool-calls)
|
||||||
|
assistant-msg (select-keys message [:role :content :tool_calls])
|
||||||
|
new-messages (into (conj messages assistant-msg) tool-results)]
|
||||||
|
(recur new-messages (inc iteration) signatures))))
|
||||||
|
|
||||||
;; Done
|
;; Done
|
||||||
(do
|
(do
|
||||||
(println "\n[Done]")
|
(log "Agent finished after" iteration "iterations")
|
||||||
response))))))
|
response))))))
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
@@ -231,7 +340,7 @@
|
|||||||
(println)
|
(println)
|
||||||
(println "Environment variables:")
|
(println "Environment variables:")
|
||||||
(println " OLLAMA_HOST - Ollama server URL (default: http://localhost:11434)")
|
(println " OLLAMA_HOST - Ollama server URL (default: http://localhost:11434)")
|
||||||
(println " AGENT_MODEL - Model to use (default: qwen2.5:1.5b)"))
|
(println " AGENT_MODEL - Model to use (default: qwen3-coder-next)"))
|
||||||
(run-agent (str/join " " args))))
|
(run-agent (str/join " " args))))
|
||||||
|
|
||||||
(when (= *file* (System/getProperty "babashka.file"))
|
(when (= *file* (System/getProperty "babashka.file"))
|
||||||
|
|||||||
Reference in New Issue
Block a user