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]
|
||||
[cheshire.core :as json]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]))
|
||||
[clojure.string :as str])
|
||||
(:import [java.time LocalDateTime]
|
||||
[java.time.format DateTimeFormatter]))
|
||||
|
||||
;; ============================================================
|
||||
;; Configuration
|
||||
@@ -11,12 +13,31 @@
|
||||
;; Ollama - local LLM server at http://localhost:11434
|
||||
(def ollama-host (or (System/getenv "OLLAMA_HOST") "http://localhost:11434"))
|
||||
|
||||
;; Local models with tool use support:
|
||||
;; - qwen2.5:1.5b (small, fast)
|
||||
;; - qwen2.5:3b (better quality)
|
||||
;; - llama3.2:3b (alternative)
|
||||
(def model (or (System/getenv "AGENT_MODEL") "qwen2.5:1.5b"))
|
||||
(def max-tokens 4096)
|
||||
;; Local model with tool use support
|
||||
(def model (or (System/getenv "AGENT_MODEL") "qwen3-coder-next"))
|
||||
(def max-tokens 8192)
|
||||
|
||||
;; ============================================================
|
||||
;; 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
|
||||
@@ -30,16 +51,30 @@
|
||||
(slurp file)
|
||||
(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
|
||||
"List files in a directory. Directories have trailing slashes."
|
||||
"List files in a directory with metadata (type, size, permissions)."
|
||||
[{:keys [path]}]
|
||||
(let [dir (io/file (or path "."))]
|
||||
(if (.isDirectory dir)
|
||||
(->> (.listFiles dir)
|
||||
(map (fn [f]
|
||||
(if (.isDirectory f)
|
||||
(cond
|
||||
(.isDirectory 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)
|
||||
(str/join "\n"))
|
||||
(str "Error: Not a directory: " path))))
|
||||
@@ -75,6 +110,18 @@
|
||||
(spit file content)
|
||||
(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)
|
||||
;; ============================================================
|
||||
@@ -91,7 +138,7 @@
|
||||
|
||||
{:type "function"
|
||||
: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"
|
||||
:properties {:path {:type "string"
|
||||
:description "The relative path to list (defaults to current directory)"}}
|
||||
@@ -120,7 +167,16 @@
|
||||
:content {:type "string"
|
||||
:description "The content to write to the file"}}
|
||||
: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
|
||||
(into {} (map (fn [t] [(get-in t [:function :name]) (:impl t)]) tools)))
|
||||
@@ -159,6 +215,13 @@
|
||||
[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
|
||||
"Execute a tool and return the result message."
|
||||
[{:keys [id function]}]
|
||||
@@ -168,31 +231,56 @@
|
||||
args (if (string? raw-args)
|
||||
(json/parse-string raw-args true)
|
||||
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)
|
||||
result (if tool-fn
|
||||
(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"
|
||||
:tool_call_id id
|
||||
: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
|
||||
"Run the agent loop with the given initial prompt."
|
||||
[prompt]
|
||||
(println (str "Using model: " model " via " ollama-host))
|
||||
(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."]
|
||||
(init-log!)
|
||||
(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}]
|
||||
iteration 0]
|
||||
iteration 0
|
||||
tool-sigs []]
|
||||
(when (> iteration 50)
|
||||
(println "Max iterations reached. Stopping.")
|
||||
(log "Max iterations (50) reached. Stopping.")
|
||||
(println "\nStopping: too many iterations.")
|
||||
(System/exit 1))
|
||||
|
||||
(println (str "\n[Iteration " iteration "]"))
|
||||
(log "Iteration" iteration "| messages:" (count messages))
|
||||
|
||||
(let [response (call-llm messages system-prompt)
|
||||
_ (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))
|
||||
choice (first (:choices response))
|
||||
message (:message choice)
|
||||
@@ -200,21 +288,42 @@
|
||||
content (:content message)
|
||||
tool-calls (extract-tool-calls message)]
|
||||
|
||||
(log "finish_reason:" finish-reason "| tool_calls:" (count (or tool-calls [])))
|
||||
|
||||
;; Print assistant's text response
|
||||
(when (and content (seq content))
|
||||
(println)
|
||||
(println content))
|
||||
|
||||
(if (and (= finish-reason "tool_calls") (seq tool-calls))
|
||||
;; Execute tools and continue
|
||||
(let [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)))
|
||||
;; Execute tools and continue — but check for stuck loops first
|
||||
(let [{:keys [signatures stuck?]} (detect-stuck-loop tool-calls tool-sigs repeat-threshold)]
|
||||
(if stuck?
|
||||
(do
|
||||
(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
|
||||
(do
|
||||
(println "\n[Done]")
|
||||
(log "Agent finished after" iteration "iterations")
|
||||
response))))))
|
||||
|
||||
;; ============================================================
|
||||
@@ -231,7 +340,7 @@
|
||||
(println)
|
||||
(println "Environment variables:")
|
||||
(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))))
|
||||
|
||||
(when (= *file* (System/getProperty "babashka.file"))
|
||||
|
||||
Reference in New Issue
Block a user