add diff links (for quick edits)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
logs/
|
||||
.clj-kondo/
|
||||
.lsp/
|
||||
.cpcache/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{:paths ["src"]
|
||||
:deps {tui/tui {:git/url "https://git.ajet.fyi/ajet/clojure-tui.git"
|
||||
:git/sha "4a051304888d5b4d937262decba919e1a79dd03d"}}}
|
||||
+103
-81
@@ -132,12 +132,13 @@
|
||||
change-start (if has-header?
|
||||
(or (some-> (re-find #":(\d+)" header) second parse-long) 1)
|
||||
1)
|
||||
;; Extract file extension from @@ header for syntax highlighting
|
||||
diff-lang (when has-header?
|
||||
(when-let [path (second (re-find #"@@\s+(?:new:\s+)?(\S+)" header))]
|
||||
(let [dot-idx (str/last-index-of path ".")]
|
||||
(when dot-idx
|
||||
(syntax/lang-for-ext (subs path dot-idx))))))
|
||||
;; Extract file path and extension from @@ header
|
||||
diff-path (when has-header?
|
||||
(second (re-find #"@@\s+(?:new:\s+)?(\S+)" header)))
|
||||
diff-lang (when diff-path
|
||||
(let [dot-idx (str/last-index-of diff-path ".")]
|
||||
(when dot-idx
|
||||
(syntax/lang-for-ext (subs diff-path dot-idx)))))
|
||||
body-lines (if has-header? (rest raw-lines) raw-lines)
|
||||
ctx-before (count (take-while #(str/starts-with? % " ") body-lines))
|
||||
first-lineno (- change-start ctx-before)
|
||||
@@ -145,35 +146,42 @@
|
||||
rst "\033[0m"
|
||||
[body-els _]
|
||||
(reduce
|
||||
(fn [[acc {:keys [old-n new-n]}] line]
|
||||
(cond
|
||||
(str/starts-with? line "-")
|
||||
(let [code (subs line 1)
|
||||
highlighted (str "-" (syntax/highlight-line code diff-lang "\033[38;5;210m") rst)]
|
||||
[(conj acc [:text (str " " grey (format "%4d " old-n) rst
|
||||
"\033[48;5;52m" highlighted)])
|
||||
{:old-n (inc old-n) :new-n new-n}])
|
||||
(fn [[acc {:keys [old-n new-n]}] line]
|
||||
(cond
|
||||
(str/starts-with? line "-")
|
||||
(let [code (subs line 1)
|
||||
highlighted (str "-" (syntax/highlight-line code diff-lang "\033[38;5;210m") rst)]
|
||||
[(conj acc [:text (str " " grey (format "%4d " old-n) rst
|
||||
"\033[48;5;52m" highlighted)])
|
||||
{:old-n (inc old-n) :new-n new-n}])
|
||||
|
||||
(str/starts-with? line "+")
|
||||
(let [code (subs line 1)
|
||||
highlighted (str "+" (syntax/highlight-line code diff-lang "\033[38;5;114m") rst)]
|
||||
[(conj acc [:text (str " " grey (format "%4d " new-n) rst
|
||||
"\033[48;5;22m" highlighted)])
|
||||
{:old-n old-n :new-n (inc new-n)}])
|
||||
(str/starts-with? line "+")
|
||||
(let [code (subs line 1)
|
||||
highlighted (str "+" (syntax/highlight-line code diff-lang "\033[38;5;114m") rst)]
|
||||
[(conj acc [:text (str " " grey (format "%4d " new-n) rst
|
||||
"\033[48;5;22m" highlighted)])
|
||||
{:old-n old-n :new-n (inc new-n)}])
|
||||
|
||||
(str/starts-with? line "...")
|
||||
[(conj acc [:text {:dim true} (str " " line)])
|
||||
{:old-n old-n :new-n new-n}]
|
||||
(str/starts-with? line "...")
|
||||
[(conj acc [:text {:dim true} (str " " line)])
|
||||
{:old-n old-n :new-n new-n}]
|
||||
|
||||
:else
|
||||
(let [code (if (str/starts-with? line " ") (subs line 1) line)
|
||||
highlighted (str " " (syntax/highlight-line code diff-lang "") rst)]
|
||||
[(conj acc [:text {:dim true} (str " " grey (format "%4d " old-n) rst highlighted)])
|
||||
{:old-n (inc old-n) :new-n (inc new-n)}])))
|
||||
[[] {:old-n first-lineno :new-n first-lineno}]
|
||||
body-lines)]
|
||||
:else
|
||||
(let [code (if (str/starts-with? line " ") (subs line 1) line)
|
||||
highlighted (str " " (syntax/highlight-line code diff-lang "") rst)]
|
||||
[(conj acc [:text {:dim true} (str " " grey (format "%4d " old-n) rst highlighted)])
|
||||
{:old-n (inc old-n) :new-n (inc new-n)}])))
|
||||
[[] {:old-n first-lineno :new-n first-lineno}]
|
||||
body-lines)]
|
||||
(into (if has-header?
|
||||
[[:text (str " \033[36;2m" header "\033[0m")]]
|
||||
(let [link (when diff-path
|
||||
(let [abs (if (str/starts-with? diff-path "/")
|
||||
diff-path
|
||||
(str (System/getProperty "user.dir") "/" diff-path))]
|
||||
(str abs ":" change-start)))]
|
||||
(cond-> []
|
||||
link (conj [:text {:dim true} (str " " link)])
|
||||
true (conj [:text (str " \033[36;2m" header "\033[0m")])))
|
||||
[])
|
||||
body-els))
|
||||
|
||||
@@ -385,59 +393,73 @@
|
||||
opts {}]
|
||||
(cond
|
||||
(nil? arg) opts
|
||||
(contains? #{"--help" "-h"} arg) (assoc opts :help? true)
|
||||
(= arg "--continue") (recur rest (assoc opts :continue? true))
|
||||
(= arg "--session") (let [[id & rest'] rest]
|
||||
(recur rest' (assoc opts :session-id id)))
|
||||
:else (assoc opts :prompt (str/join " " (cons arg rest))))))
|
||||
|
||||
(defn -main [& args]
|
||||
(let [{:keys [continue? session-id prompt]} (parse-args args)
|
||||
;; Load project context and skills
|
||||
project-context (context/load-project-context)
|
||||
skills (context/load-skills)
|
||||
_ (reset! core/skills-atom skills)
|
||||
system-prompt (core/build-system-prompt project-context skills)
|
||||
;; Resolve session to resume
|
||||
resume-id (cond
|
||||
session-id session-id
|
||||
continue? (core/latest-session-id))
|
||||
resumed (when resume-id (core/load-session resume-id))
|
||||
;; Session ID: reuse existing or generate new
|
||||
sid (or resume-id (core/new-session-id))
|
||||
created (or (:created resumed) (str (java.time.Instant/now)))
|
||||
;; Build initial state from resumed session or fresh
|
||||
base-conversation (or (:conversation resumed) [])
|
||||
base-messages (or (:messages resumed) [])
|
||||
eq (make-event-queue)
|
||||
;; If there's a prompt, append it and start agent loop
|
||||
[conversation messages start?]
|
||||
(if prompt
|
||||
[(conj base-conversation {:role "user" :content prompt})
|
||||
(conj base-messages {:role :user :content prompt})
|
||||
true]
|
||||
[base-conversation base-messages false])
|
||||
agent-handle (when start? (core/run-agent-loop! system-prompt conversation eq))
|
||||
initial-model {:messages messages
|
||||
:input ""
|
||||
:conversation conversation
|
||||
:event-queue eq
|
||||
:session-id sid
|
||||
:created created
|
||||
:system-prompt system-prompt
|
||||
:skills skills
|
||||
:agent-running? start?
|
||||
:agent-handle agent-handle
|
||||
:spinner-frame 0
|
||||
:scroll-offset 0}
|
||||
initial-events (when start?
|
||||
[(ev/delayed-event 100 {:type :poll})
|
||||
(ev/delayed-event 80 {:type :spinner})])]
|
||||
(tui/run
|
||||
{:init initial-model
|
||||
:update update-fn
|
||||
:view view
|
||||
:fps 30
|
||||
:init-events initial-events})
|
||||
;; Post-exit: print session info
|
||||
(println (str "\nSession: " sid))
|
||||
(println (str "To continue: agent --session " sid))))
|
||||
(let [{:keys [help? continue? session-id prompt]} (parse-args args)]
|
||||
(when help?
|
||||
(println "Usage: agent [options] [prompt]")
|
||||
(println)
|
||||
(println "Options:")
|
||||
(println " --help, -h Show this help message")
|
||||
(println " --continue Resume the most recent session")
|
||||
(println " --session <id> Resume a specific session by ID")
|
||||
(println)
|
||||
(println "Examples:")
|
||||
(println " agent Start a new interactive session")
|
||||
(println " agent \"fix the bug\" Start with an initial prompt")
|
||||
(println " agent --continue Resume the last session")
|
||||
(System/exit 0))
|
||||
(let [;; Load project context and skills
|
||||
project-context (context/load-project-context)
|
||||
skills (context/load-skills)
|
||||
_ (reset! core/skills-atom skills)
|
||||
system-prompt (core/build-system-prompt project-context skills)
|
||||
;; Resolve session to resume
|
||||
resume-id (cond
|
||||
session-id session-id
|
||||
continue? (core/latest-session-id))
|
||||
resumed (when resume-id (core/load-session resume-id))
|
||||
;; Session ID: reuse existing or generate new
|
||||
sid (or resume-id (core/new-session-id))
|
||||
created (or (:created resumed) (str (java.time.Instant/now)))
|
||||
;; Build initial state from resumed session or fresh
|
||||
base-conversation (or (:conversation resumed) [])
|
||||
base-messages (or (:messages resumed) [])
|
||||
eq (make-event-queue)
|
||||
;; If there's a prompt, append it and start agent loop
|
||||
[conversation messages start?]
|
||||
(if prompt
|
||||
[(conj base-conversation {:role "user" :content prompt})
|
||||
(conj base-messages {:role :user :content prompt})
|
||||
true]
|
||||
[base-conversation base-messages false])
|
||||
agent-handle (when start? (core/run-agent-loop! system-prompt conversation eq))
|
||||
initial-model {:messages messages
|
||||
:input ""
|
||||
:conversation conversation
|
||||
:event-queue eq
|
||||
:session-id sid
|
||||
:created created
|
||||
:system-prompt system-prompt
|
||||
:skills skills
|
||||
:agent-running? start?
|
||||
:agent-handle agent-handle
|
||||
:spinner-frame 0
|
||||
:scroll-offset 0}
|
||||
initial-events (when start?
|
||||
[(ev/delayed-event 100 {:type :poll})
|
||||
(ev/delayed-event 80 {:type :spinner})])]
|
||||
(tui/run
|
||||
{:init initial-model
|
||||
:update update-fn
|
||||
:view view
|
||||
:fps 30
|
||||
:init-events initial-events})
|
||||
;; Post-exit: print session info
|
||||
(println (str "\nSession: " sid))
|
||||
(println (str "To continue: agent --session " sid)))))
|
||||
|
||||
+189
-86
@@ -150,8 +150,8 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
selected (cond->> (drop start lines)
|
||||
limit (take limit))
|
||||
numbered (map-indexed
|
||||
(fn [i line] (str (+ start i 1) "\t" line))
|
||||
selected)]
|
||||
(fn [i line] (str (+ start i 1) "\t" line))
|
||||
selected)]
|
||||
(str (str/join "\n" numbered)
|
||||
"\n[" total " lines total]")))))
|
||||
|
||||
@@ -190,11 +190,11 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
before-ctx (subvec all-lines ctx-start start-line)
|
||||
after-ctx (subvec all-lines end-line ctx-end)
|
||||
diff-lines (concat
|
||||
[(str "@@ " path ":" (inc start-line) " @@")]
|
||||
(map #(str " " %) before-ctx)
|
||||
(map #(str "-" %) old-lines)
|
||||
(map #(str "+" %) new-lines)
|
||||
(map #(str " " %) after-ctx))
|
||||
[(str "@@ " path ":" (inc start-line) " @@")]
|
||||
(map #(str " " %) before-ctx)
|
||||
(map #(str "-" %) old-lines)
|
||||
(map #(str "+" %) new-lines)
|
||||
(map #(str " " %) after-ctx))
|
||||
max-lines 30]
|
||||
(str/join "\n" (if (> (count diff-lines) max-lines)
|
||||
(concat (take max-lines diff-lines) ["..."])
|
||||
@@ -228,11 +228,11 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
truncated? (> (count lines) max-lines)
|
||||
shown (if truncated? (take max-lines lines) lines)
|
||||
diff (str/join "\n"
|
||||
(concat
|
||||
[(str "@@ new: " path " @@")]
|
||||
(map #(str "+" %) shown)
|
||||
(when truncated?
|
||||
[(str "... +" (- (count lines) max-lines) " more lines")])))]
|
||||
(concat
|
||||
[(str "@@ new: " path " @@")]
|
||||
(map #(str "+" %) shown)
|
||||
(when truncated?
|
||||
[(str "... +" (- (count lines) max-lines) " more lines")])))]
|
||||
(io/make-parents file)
|
||||
(spit file content)
|
||||
{:message (str "Successfully created " path)
|
||||
@@ -275,24 +275,24 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
(not (ignored-path? base-dir %))
|
||||
(<= (fs/size %) (* 1024 1024)))))
|
||||
results (reduce
|
||||
(fn [acc file]
|
||||
(if (>= (count acc) max-matches)
|
||||
(reduced acc)
|
||||
(try
|
||||
(let [relative (str (fs/relativize base-dir file))
|
||||
lines (str/split-lines (slurp (fs/file file)))]
|
||||
(reduce
|
||||
(fn [acc2 [idx line]]
|
||||
(if (>= (count acc2) max-matches)
|
||||
(reduced acc2)
|
||||
(if (.find (.matcher compiled line))
|
||||
(conj acc2 (str relative ":" (inc idx) ":" line))
|
||||
acc2)))
|
||||
acc
|
||||
(map-indexed vector lines)))
|
||||
(catch Exception _ acc))))
|
||||
[]
|
||||
files)
|
||||
(fn [acc file]
|
||||
(if (>= (count acc) max-matches)
|
||||
(reduced acc)
|
||||
(try
|
||||
(let [relative (str (fs/relativize base-dir file))
|
||||
lines (str/split-lines (slurp (fs/file file)))]
|
||||
(reduce
|
||||
(fn [acc2 [idx line]]
|
||||
(if (>= (count acc2) max-matches)
|
||||
(reduced acc2)
|
||||
(if (.find (.matcher compiled line))
|
||||
(conj acc2 (str relative ":" (inc idx) ":" line))
|
||||
acc2)))
|
||||
acc
|
||||
(map-indexed vector lines)))
|
||||
(catch Exception _ acc))))
|
||||
[]
|
||||
files)
|
||||
truncated? (>= (count results) max-matches)]
|
||||
(if (seq results)
|
||||
(cond-> (str/join "\n" results)
|
||||
@@ -515,7 +515,9 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
{:choices [{:message (:message result)
|
||||
:finish_reason (if (seq (get-in result [:message :tool_calls]))
|
||||
"tool_calls"
|
||||
"stop")}]}))
|
||||
"stop")}]
|
||||
:usage {:prompt_tokens (:prompt_eval_count result)
|
||||
:completion_tokens (:eval_count result)}}))
|
||||
|
||||
;; ============================================================
|
||||
;; Tool Execution
|
||||
@@ -578,10 +580,28 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
(defn- tool-call-names [tool-calls]
|
||||
(mapv #(get-in % [:function :name]) tool-calls))
|
||||
|
||||
(defn- detect-name-cycle
|
||||
"Detects repeating cycles of tool names regardless of arguments.
|
||||
e.g. [rm create_file rm create_file] has a cycle of length 2 repeating 2x.
|
||||
For single-tool cycles (length 1), requires 4 repeats to avoid false positives
|
||||
on legitimate back-to-back calls like read_file, read_file.
|
||||
Returns the cycle length if detected, else nil."
|
||||
[names-history min-repeats]
|
||||
(let [n (count names-history)]
|
||||
(when (>= n (* 2 min-repeats))
|
||||
(first
|
||||
(for [cycle-len (range 2 (inc (quot n min-repeats)))
|
||||
:let [recent (take-last (* cycle-len min-repeats) names-history)
|
||||
chunks (partition cycle-len recent)]
|
||||
:when (and (= (count chunks) min-repeats)
|
||||
(apply = chunks))]
|
||||
cycle-len)))))
|
||||
|
||||
(defn- detect-stuck-loop
|
||||
"Detects two kinds of stuck loops:
|
||||
"Detects three kinds of stuck loops:
|
||||
1. Exact repeat: identical tool calls N times in a row (hard stop)
|
||||
2. Research loop: same tool(s) called N+ times with varying args (nudge, then stop)
|
||||
2. Name cycle: same pattern of tool names repeating with varying args (hard stop)
|
||||
3. Research loop: delegate called N+ times with varying args (nudge, then stop)
|
||||
Returns {:signatures, :stuck? (tool name or nil), :nudge? bool}"
|
||||
[tool-calls previous-signatures repeat-threshold]
|
||||
(let [current-sigs (mapv tool-call-signature tool-calls)
|
||||
@@ -591,23 +611,64 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
(let [recent (take-last repeat-threshold all-sigs)]
|
||||
(when (apply = recent)
|
||||
(ffirst (last recent)))))
|
||||
;; Research loop detection: same tool name(s) used N consecutive times
|
||||
;; with different arguments (e.g. varied web_search queries)
|
||||
names-history (mapv (fn [sigs] (set (map first sigs))) all-sigs)
|
||||
;; Name cycle detection: same tool name pattern repeating with different args
|
||||
;; Catches e.g. [rm, create_file, rm, create_file] even when file content varies
|
||||
names-history (mapv (fn [sigs] (mapv first sigs)) all-sigs)
|
||||
;; 2 full repetitions of a cycle is enough to detect (e.g. rm,create,rm,create)
|
||||
name-cycle? (when-not exact-stuck?
|
||||
(detect-name-cycle names-history 2))
|
||||
;; Research loop detection: delegate called N+ times with varying args
|
||||
name-sets (mapv set names-history)
|
||||
research-tools #{"delegate"}
|
||||
consecutive-research
|
||||
(count (take-while #(and (seq (set/intersection % research-tools))
|
||||
(every? research-tools %))
|
||||
(reverse names-history)))
|
||||
(reverse name-sets)))
|
||||
nudge-threshold 3
|
||||
hard-research-limit 6]
|
||||
{:signatures all-sigs
|
||||
:stuck? (or exact-stuck?
|
||||
(when name-cycle?
|
||||
(str "name cycle (length " name-cycle? ")"))
|
||||
(when (>= consecutive-research hard-research-limit)
|
||||
"web_search"))
|
||||
:nudge? (and (not exact-stuck?)
|
||||
(not name-cycle?)
|
||||
(= consecutive-research nudge-threshold))}))
|
||||
|
||||
;; ============================================================
|
||||
;; Context Window Management
|
||||
;; ============================================================
|
||||
|
||||
(defn- get-model-context-length
|
||||
"Query Ollama for the model's context window size. Returns nil on failure."
|
||||
[]
|
||||
(try
|
||||
(let [response (http/post (str ollama-host "/api/show")
|
||||
{:headers {"Content-Type" "application/json"}
|
||||
:body (json/generate-string {:model model})
|
||||
:timeout 5000})
|
||||
result (json/parse-string (:body response) true)
|
||||
info (:model_info result)]
|
||||
;; Context length key varies by architecture (e.g. "qwen3next.context_length", "llama.context_length")
|
||||
(some (fn [[k v]] (when (str/ends-with? (name k) ".context_length") v)) info))
|
||||
(catch Exception _ nil)))
|
||||
|
||||
(defn- trim-messages
|
||||
"Reduce conversation size by truncating old tool result content.
|
||||
Keeps the last `keep-recent` messages intact."
|
||||
[messages keep-recent]
|
||||
(let [n (count messages)
|
||||
cutoff (max 0 (- n keep-recent))]
|
||||
(mapv (fn [i msg]
|
||||
(if (and (< i cutoff)
|
||||
(= (:role msg) "tool")
|
||||
(string? (:content msg))
|
||||
(> (count (:content msg)) 100))
|
||||
(assoc msg :content "[previous tool result truncated]")
|
||||
msg))
|
||||
(range) messages)))
|
||||
|
||||
;; ============================================================
|
||||
;; Async Agent Loop
|
||||
;; ============================================================
|
||||
@@ -622,15 +683,18 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
Returns {:future f :cancel! cancel-atom}."
|
||||
[system-prompt conversation event-queue]
|
||||
(let [log-file (init-log)
|
||||
cancelled? (atom false)]
|
||||
(log log-file "Agent loop started | model:" model "| messages:" (count conversation))
|
||||
cancelled? (atom false)
|
||||
context-length (get-model-context-length)]
|
||||
(log log-file "Agent loop started | model:" model "| messages:" (count conversation)
|
||||
(if context-length (str "| context_length: " context-length) ""))
|
||||
{:cancel! cancelled?
|
||||
:future
|
||||
(future
|
||||
(try
|
||||
(loop [messages conversation
|
||||
iteration 0
|
||||
tool-sigs []]
|
||||
tool-sigs []
|
||||
retries 0]
|
||||
(cond
|
||||
@cancelled?
|
||||
(do
|
||||
@@ -647,55 +711,94 @@ Always explain what you're doing before using tools. Use the tools when needed t
|
||||
:else
|
||||
(do
|
||||
(log log-file "Iteration" iteration "| messages:" (count messages))
|
||||
(let [response (call-llm system-prompt messages)
|
||||
choice (first (:choices response))
|
||||
message (:message choice)
|
||||
finish-reason (:finish_reason choice)
|
||||
content (:content message)
|
||||
tool-calls (:tool_calls message)]
|
||||
(log log-file "finish_reason:" finish-reason "| tool_calls:" (count (or tool-calls [])))
|
||||
;; Push assistant text
|
||||
(when (and content (seq (str/trim content)))
|
||||
(swap! event-queue conj {:type :text :content content}))
|
||||
;; Handle tool calls or finish
|
||||
(if (and (= finish-reason "tool_calls") (seq tool-calls))
|
||||
(let [{:keys [signatures stuck? nudge?]} (detect-stuck-loop tool-calls tool-sigs 3)]
|
||||
(cond
|
||||
stuck?
|
||||
(let [[response error] (try [(call-llm system-prompt messages) nil]
|
||||
(catch Exception e [nil e]))]
|
||||
(if error
|
||||
;; LLM call failed — attempt recovery
|
||||
(if @cancelled?
|
||||
(do
|
||||
(log log-file "Agent interrupted by user during LLM call")
|
||||
(swap! event-queue conj {:type :error :message "Interrupted."})
|
||||
(swap! event-queue conj {:type :done :conversation messages}))
|
||||
(if (< retries 2)
|
||||
(let [keep-n (if (zero? retries) 10 4)
|
||||
trimmed (trim-messages messages keep-n)]
|
||||
(log log-file "LLM call failed:" (str error) "— retry" (inc retries) "with trimmed context (keeping last" keep-n ")")
|
||||
(swap! event-queue conj {:type :error :message (str "LLM error, trimming context and retrying... (" (str error) ")")})
|
||||
(recur trimmed iteration tool-sigs (inc retries)))
|
||||
(do
|
||||
(log log-file "Stuck loop detected:" stuck?)
|
||||
(swap! event-queue conj {:type :error :message "Agent is repeating the same action."})
|
||||
(swap! event-queue conj {:type :done :conversation messages}))
|
||||
(log log-file "LLM call failed after retries:" (str error))
|
||||
(swap! event-queue conj {:type :error :message (str error)})
|
||||
(swap! event-queue conj {:type :done :conversation messages}))))
|
||||
|
||||
:else
|
||||
;; LLM call succeeded
|
||||
(let [choice (first (:choices response))
|
||||
message (:message choice)
|
||||
finish-reason (:finish_reason choice)
|
||||
content (:content message)
|
||||
tool-calls (:tool_calls message)
|
||||
;; Context window tracking
|
||||
prompt-tokens (get-in response [:usage :prompt_tokens])
|
||||
completion-tokens (get-in response [:usage :completion_tokens])]
|
||||
(log log-file "finish_reason:" finish-reason
|
||||
"| tool_calls:" (count (or tool-calls []))
|
||||
(when prompt-tokens (str "| tokens: " prompt-tokens "/" (or context-length "?")
|
||||
" (" (when context-length (str (int (* 100 (/ prompt-tokens context-length))) "%")) ")")))
|
||||
;; Warn user when approaching context limit
|
||||
(when (and context-length prompt-tokens
|
||||
(> (/ prompt-tokens context-length) 0.80))
|
||||
(let [pct (int (* 100 (/ prompt-tokens context-length)))]
|
||||
(log log-file "Context window warning:" pct "% used")
|
||||
(swap! event-queue conj
|
||||
{:type :error
|
||||
:message (str "Context " pct "% full (" prompt-tokens "/" context-length " tokens). Responses may degrade.")})))
|
||||
;; Push assistant text
|
||||
(when (and content (seq (str/trim content)))
|
||||
(swap! event-queue conj {:type :text :content content}))
|
||||
;; Handle tool calls or finish
|
||||
(if (and (= finish-reason "tool_calls") (seq tool-calls))
|
||||
(let [{:keys [signatures stuck? nudge?]} (detect-stuck-loop tool-calls tool-sigs 3)]
|
||||
(cond
|
||||
stuck?
|
||||
(do
|
||||
(log log-file "Stuck loop detected:" stuck?)
|
||||
(swap! event-queue conj {:type :error :message "Agent is repeating the same action."})
|
||||
(swap! event-queue conj {:type :done :conversation messages}))
|
||||
|
||||
:else
|
||||
(do
|
||||
;; Show tool activities
|
||||
(doseq [tc tool-calls]
|
||||
(swap! event-queue conj {:type :tool :label (tool-call-label tc)}))
|
||||
;; Execute tools
|
||||
(let [tool-results (mapv #(execute-tool log-file %) tool-calls)]
|
||||
;; Push diffs to UI
|
||||
(doseq [tr tool-results]
|
||||
(when-let [diff (:diff tr)]
|
||||
(swap! event-queue conj {:type :diff :content diff})))
|
||||
(let [clean-results (mapv #(dissoc % :diff) tool-results)
|
||||
assistant-msg (select-keys message [:role :content :tool_calls])
|
||||
new-messages (into (conj messages assistant-msg) clean-results)
|
||||
;; If nudge, inject a system hint to stop researching
|
||||
new-messages (if nudge?
|
||||
(do (log log-file "Research loop nudge injected")
|
||||
(conj new-messages
|
||||
{:role "system"
|
||||
:content "You have already performed several web searches. You have enough information to answer. Stop searching and synthesize your findings into a clear response now."}))
|
||||
new-messages)]
|
||||
(recur new-messages (inc iteration) signatures 0))))))
|
||||
;; Done - no more tool calls
|
||||
(do
|
||||
;; Show tool activities
|
||||
(doseq [tc tool-calls]
|
||||
(swap! event-queue conj {:type :tool :label (tool-call-label tc)}))
|
||||
;; Execute tools
|
||||
(let [tool-results (mapv #(execute-tool log-file %) tool-calls)]
|
||||
;; Push diffs to UI
|
||||
(doseq [tr tool-results]
|
||||
(when-let [diff (:diff tr)]
|
||||
(swap! event-queue conj {:type :diff :content diff})))
|
||||
(let [clean-results (mapv #(dissoc % :diff) tool-results)
|
||||
assistant-msg (select-keys message [:role :content :tool_calls])
|
||||
new-messages (into (conj messages assistant-msg) clean-results)
|
||||
;; If nudge, inject a system hint to stop researching
|
||||
new-messages (if nudge?
|
||||
(do (log log-file "Research loop nudge injected")
|
||||
(conj new-messages
|
||||
{:role "system"
|
||||
:content "You have already performed several web searches. You have enough information to answer. Stop searching and synthesize your findings into a clear response now."}))
|
||||
new-messages)]
|
||||
(recur new-messages (inc iteration) signatures))))))
|
||||
;; Done - no more tool calls
|
||||
(do
|
||||
(log log-file "Agent finished after" iteration "iterations")
|
||||
(swap! event-queue conj
|
||||
{:type :done
|
||||
:conversation (conj messages
|
||||
(select-keys message [:role :content]))})))))))
|
||||
(log log-file "Agent finished after" iteration "iterations")
|
||||
(swap! event-queue conj
|
||||
{:type :done
|
||||
:conversation (conj messages
|
||||
(select-keys message [:role :content]))})))))))))
|
||||
(catch java.net.ConnectException _
|
||||
(let [msg (str "Could not reach model server at " ollama-host " — is Ollama running?")]
|
||||
(log log-file "Agent error:" msg)
|
||||
(swap! event-queue conj {:type :error :message msg})
|
||||
(swap! event-queue conj {:type :done :conversation conversation})))
|
||||
(catch Exception e
|
||||
(log log-file "Agent error:" (str e))
|
||||
(swap! event-queue conj {:type :error :message (str e)})
|
||||
|
||||
Reference in New Issue
Block a user