Skip to content

Commit fe34f47

Browse files
committed
Merge branch 'master' into remote
2 parents 973544e + ae9f4be commit fe34f47

2 files changed

Lines changed: 67 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
- Auto-retry Anthropic streams that end prematurely with empty responses, and auto-continue when content was partially received.
5+
- Auto-retry Anthropic streams that end prematurely with empty responses, and auto-continue when response is truncated (e.g. unclosed code blocks).
66
- Add remote web control server for browser-based chat observation and control via `web.eca.dev`. #333
77

88
## 0.115.5

src/eca/features/chat.clj

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@
333333
{:type :prompt-message
334334
:message message})))
335335

336+
(defn ^:private truncated-response?
337+
"Returns true when the response text shows signs of being truncated mid-stream.
338+
Checks for unclosed code fences (odd number of ``` markers at line start)."
339+
[^String text]
340+
(when-not (string/blank? text)
341+
(odd? (count (re-seq #"(?m)^```" text)))))
342+
336343
(defn ^:private trigger-auto-compact!
337344
"Trigger auto-compact: send compact prompt, then resume the original task."
338345
[{:keys [db* config chat-id agent] :as chat-ctx}
@@ -527,27 +534,32 @@
527534
:text (str "API limit reached. Tokens: "
528535
(json/generate-string (:tokens msg)))})
529536
(lifecycle/finish-chat-prompt! :idle (dissoc chat-ctx :on-finished-side-effect)))
530-
:finish (do (add-to-history! {:role "assistant"
531-
:content [{:type :text :text @received-msgs*}]})
532-
(if (and (:premature? msg)
533-
(not (:auto-continued? chat-ctx))
534-
(not (:on-finished-side-effect chat-ctx)))
535-
(do
536-
(logger/info logger-tag "Premature stream stop detected, auto-continuing" {:chat-id chat-id})
537-
(lifecycle/send-content! chat-ctx :system
538-
{:type :text :text "Response was interrupted. Continuing..."})
539-
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
540-
(lifecycle/finish-chat-prompt! :idle
541-
(assoc chat-ctx :on-finished-side-effect
542-
(fn []
543-
(swap! db* update-in [:chats chat-id] dissoc :auto-compacting?)
544-
(prompt-messages!
545-
[{:role "user"
546-
:content [{:type :text
547-
:text "Your previous response was interrupted mid-stream. Continue from where you left off."}]}]
548-
:auto-continue
549-
(assoc chat-ctx :auto-continued? true))))))
550-
(lifecycle/finish-chat-prompt! :idle chat-ctx)))))
537+
:finish (let [response-text @received-msgs*]
538+
(add-to-history! {:role "assistant"
539+
:content [{:type :text :text response-text}]})
540+
(if (and (or (:premature? msg)
541+
(truncated-response? response-text))
542+
(not (:auto-continued? chat-ctx))
543+
(not (:on-finished-side-effect chat-ctx)))
544+
(do
545+
(logger/info logger-tag "Truncated or premature response detected, auto-continuing"
546+
{:chat-id chat-id
547+
:premature? (:premature? msg)
548+
:truncated? (truncated-response? response-text)})
549+
(lifecycle/send-content! chat-ctx :system
550+
{:type :text :text "Response was interrupted. Continuing..."})
551+
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
552+
(lifecycle/finish-chat-prompt! :idle
553+
(assoc chat-ctx :on-finished-side-effect
554+
(fn []
555+
(swap! db* update-in [:chats chat-id] dissoc :auto-compacting?)
556+
(prompt-messages!
557+
[{:role "user"
558+
:content [{:type :text
559+
:text "Your previous response was interrupted mid-stream. Continue from where you left off, do not redo completed steps."}]}]
560+
:auto-continue
561+
(assoc chat-ctx :auto-continued? true))))))
562+
(lifecycle/finish-chat-prompt! :idle chat-ctx)))))
551563
:on-prepare-tool-call (fn [{:keys [id full-name arguments-text]}]
552564
(lifecycle/assert-chat-not-stopped! chat-ctx)
553565
(let [all-tools (f.tools/all-tools chat-id agent @db* config)
@@ -675,14 +687,44 @@
675687
:text "Context window exceeded. Auto-compacting conversation..."})
676688
(prune-tool-results! db* chat-id {})
677689
(trigger-auto-compact! chat-ctx all-tools user-messages))
678-
(do
690+
(let [partial-text @received-msgs*
691+
transient-error? (contains? #{:overloaded :premature-stop} error-type)
692+
can-auto-continue? (and (or transient-error?
693+
(string/includes? (or message "") "idle timeout"))
694+
(not (:auto-continued? chat-ctx))
695+
(not (:on-finished-side-effect chat-ctx))
696+
(not compacting?))]
679697
(when compacting?
680698
(swap! db* update-in [:chats chat-id] dissoc :auto-compacting? :compacting?))
681-
(lifecycle/send-content! chat-ctx :system {:type :text :text (or message (str "Error: " (or (ex-message exception) (.getName (class exception)))))})
682-
(lifecycle/finish-chat-prompt! :idle (dissoc chat-ctx :on-finished-side-effect))))))})
699+
(when-not (string/blank? partial-text)
700+
(add-to-history! {:role "assistant"
701+
:content [{:type :text :text partial-text}]}))
702+
(if can-auto-continue?
703+
(do
704+
(logger/info logger-tag "Transient error during response, auto-continuing"
705+
{:chat-id chat-id :error-type error-type})
706+
(lifecycle/send-content! chat-ctx :system
707+
{:type :text :text (str (or message "Connection interrupted") ". Continuing...")})
708+
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
709+
(lifecycle/finish-chat-prompt! :idle
710+
(assoc chat-ctx :on-finished-side-effect
711+
(fn []
712+
(swap! db* update-in [:chats chat-id] dissoc :auto-compacting?)
713+
(prompt-messages!
714+
[{:role "user"
715+
:content [{:type :text
716+
:text "Your previous response was interrupted mid-stream. Continue from where you left off, do not redo completed steps."}]}]
717+
:auto-continue
718+
(assoc chat-ctx :auto-continued? true))))))
719+
(do
720+
(lifecycle/send-content! chat-ctx :system {:type :text :text (or message (str "Error: " (or (ex-message exception) (.getName (class exception)))))})
721+
(lifecycle/finish-chat-prompt! :idle (dissoc chat-ctx :on-finished-side-effect))))))))})
683722
(catch Exception e
684723
(when-not (:silent? (ex-data e))
685724
(logger/error e)
725+
(when-not (string/blank? @received-msgs*)
726+
(add-to-history! {:role "assistant"
727+
:content [{:type :text :text @received-msgs*}]}))
686728
(lifecycle/send-content! chat-ctx :system {:type :text :text (str "Error: " (or (ex-message e) (.getName (class e))))})
687729
(lifecycle/finish-chat-prompt! :idle (dissoc chat-ctx :on-finished-side-effect))))
688730
(finally

0 commit comments

Comments
 (0)