Skip to content

Commit 4ce3ed6

Browse files
committed
Fix /compact triggering empty-response retries and rejected tool errors after the compact tool finishes
1 parent e08384c commit 4ce3ed6

7 files changed

Lines changed: 133 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ECA Agent Guide (AGENTS.md)
33
- Build (requires Clojure CLI + Babashka):
44
- All-in-one debug CLI (JVM, nREPL): `bb debug-cli`
55
- Production CLI (JVM): `bb prod-cli` | Production JAR: `bb prod-jar`
6-
- Native image (GraalVM, `GRAALVM_HOME` set): `bb native-cli`
6+
- In production we use a native image (GraalVM, `GRAALVM_HOME` set): `bb native-cli`
77
- Test (Kaocha via `:test` alias):
88
- Run all unit tests: `bb test` (same as `clojure -M:test`)
99
- Run a single unit test namespace: `clojure -M:test --focus eca.main-test`

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Fix `/compact` triggering empty-response retries and rejected tool errors after the compact tool finishes.
6+
57
## 0.116.6
68

79
- Bump plumcp to 0.2.0-beta5.

integration-test/integration/chat/commands_test.clj

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[integration.eca :as eca]
66
[integration.fixture :as fixture]
77
[integration.helper :refer [match-content] :as h]
8+
[llm-mock.mocks :as llm.mocks]
89
[matcher-combinators.matchers :as m]
910
[matcher-combinators.test :refer [match?]]))
1011

@@ -100,3 +101,93 @@
100101
:commands (m/embeds
101102
[{:name "mcpServerSample:my-prompt" :arguments [{:name "some-arg-1"}]}])}
102103
resp)))))
104+
105+
(deftest compact-command
106+
(eca/start-process!)
107+
108+
(eca/request! (fixture/initialize-request))
109+
(eca/notify! (fixture/initialized-notification))
110+
111+
(let [chat-id* (atom nil)]
112+
(testing "Setup: send an initial message to create chat history"
113+
(llm.mocks/set-case! :simple-text-0)
114+
(let [resp (eca/request! (fixture/chat-prompt-request
115+
{:model "anthropic/claude-sonnet-4-6"
116+
:message "Tell me a joke!"}))
117+
chat-id (reset! chat-id* (:chatId resp))]
118+
(match-content chat-id "user" {:type "text" :text "Tell me a joke!\n"})
119+
(match-content chat-id "system" {:type "metadata" :title "Some Cool Title"})
120+
(match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"})
121+
(match-content chat-id "system" {:type "progress" :state "running" :text "Generating"})
122+
(match-content chat-id "assistant" {:type "text" :text "Knock"})
123+
(match-content chat-id "assistant" {:type "text" :text " knock!"})
124+
(match-content chat-id "system" {:type "usage"})
125+
(match-content chat-id "system" {:type "progress" :state "finished"})))
126+
127+
(testing "Compact calls the tool and finishes cleanly without a second LLM request"
128+
(llm.mocks/set-case! :compact-0)
129+
(let [resp (eca/request! (fixture/chat-prompt-request
130+
{:chat-id @chat-id*
131+
:model "anthropic/claude-sonnet-4-6"
132+
:message "/compact"}))
133+
chat-id (:chatId resp)]
134+
135+
(is (match? {:chatId (m/pred string?)
136+
:model "anthropic/claude-sonnet-4-6"
137+
:status "prompting"}
138+
resp))
139+
140+
;; User message
141+
(match-content chat-id "user" {:type "text" :text "/compact\n"})
142+
;; Progress
143+
(match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"})
144+
(match-content chat-id "system" {:type "progress" :state "running" :text "Generating"})
145+
;; Tool call preparation (streaming)
146+
(match-content chat-id "assistant" {:type "toolCallPrepare"
147+
:origin "native"
148+
:id "compact-1"
149+
:name "compact_chat"
150+
:argumentsText ""
151+
:summary "Compacting..."})
152+
(match-content chat-id "assistant" {:type "toolCallPrepare"
153+
:origin "native"
154+
:id "compact-1"
155+
:name "compact_chat"
156+
:argumentsText "{\"summary\":\"Test summary of the conversation\"}"
157+
:summary "Compacting..."})
158+
;; Usage from LLM response
159+
(match-content chat-id "system" {:type "usage"})
160+
;; Tool execution
161+
(match-content chat-id "assistant" {:type "toolCallRun"
162+
:origin "native"
163+
:id "compact-1"
164+
:name "compact_chat"
165+
:arguments {:summary "Test summary of the conversation"}
166+
:manualApproval false
167+
:summary "Compacting..."})
168+
(match-content chat-id "assistant" {:type "toolCallRunning"
169+
:origin "native"
170+
:id "compact-1"
171+
:name "compact_chat"
172+
:summary "Compacting..."})
173+
(match-content chat-id "system" {:type "progress" :state "running" :text "Calling tool"})
174+
(match-content chat-id "assistant" {:type "toolCalled"
175+
:origin "native"
176+
:id "compact-1"
177+
:name "compact_chat"
178+
:error false
179+
:totalTimeMs (m/pred number?)
180+
:outputs [{:type "text" :text "Compacted successfully!"}]})
181+
;; Chat finishes, then compact side-effect sends summary messages
182+
(match-content chat-id "system" {:type "progress" :state "finished"})
183+
(match-content chat-id "system" {:type "text" :text "Compacted chat"})
184+
(match-content chat-id "system" {:type "usage"})
185+
186+
;; Key assertion: only one LLM request was made (no tool_result continuation).
187+
;; Before the fix, the continue-fn would trigger a second LLM call whose
188+
;; request body would overwrite this one and contain tool_result messages.
189+
(is (not-any? (fn [{:keys [content]}]
190+
(and (sequential? content)
191+
(some #(= "tool_result" (:type %)) content)))
192+
(:messages (llm.mocks/get-req-body :compact-0)))
193+
"Only one LLM request should be made - no tool_result continuation")))))

integration-test/llm_mock/anthropic.clj

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,27 @@
257257
(sse-send! ch "message_stop" {:type "message_stop"})
258258
(hk/close ch)))))
259259

260+
(defn ^:private compact-0 [ch]
261+
;; LLM calls eca__compact_chat with a summary — no text or reasoning
262+
(sse-send! ch "content_block_start"
263+
{:type "content_block_start"
264+
:index 0
265+
:content_block {:type "tool_use"
266+
:id "compact-1"
267+
:name "eca__compact_chat"}})
268+
(sse-send! ch "content_block_delta"
269+
{:type "content_block_delta"
270+
:index 0
271+
:delta {:type "input_json_delta"
272+
:partial_json "{\"summary\":\"Test summary of the conversation\"}"}})
273+
(sse-send! ch "message_delta"
274+
{:type "message_delta"
275+
:delta {:stop_reason "tool_use"}
276+
:usage {:input_tokens 100
277+
:output_tokens 50}})
278+
(sse-send! ch "message_stop" {:type "message_stop"})
279+
(hk/close ch))
280+
260281
(defn ^:private chat-title-text-0 [ch]
261282
(hk/send! ch
262283
(json/generate-string
@@ -286,4 +307,5 @@
286307
:reasoning-0 (reasoning-0 ch)
287308
:reasoning-1 (reasoning-1 ch)
288309
:tool-calling-0 (tool-calling-0 ch body)
289-
:mcp-tool-call-0 (mcp-tool-call-0 ch body)))))})))
310+
:mcp-tool-call-0 (mcp-tool-call-0 ch body)
311+
:compact-0 (compact-0 ch)))))})))

src/eca/features/chat.clj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -608,10 +608,14 @@
608608
:on-tools-called (tc/on-tools-called!
609609
(assoc chat-ctx :continue-fn
610610
(fn [tc-all-tools tc-user-messages]
611-
(if (lifecycle/auto-compact? chat-id agent full-model config @db*)
612-
(trigger-auto-compact! chat-ctx tc-all-tools tc-user-messages)
613-
{:tools tc-all-tools
614-
:new-messages (get-in @db* [:chats chat-id :messages])})))
611+
(if (get-in @db* [:chats chat-id :compact-done?])
612+
(do (swap! db* update-in [:chats chat-id] dissoc :compact-done?)
613+
(lifecycle/finish-chat-prompt! :idle chat-ctx)
614+
nil)
615+
(if (lifecycle/auto-compact? chat-id agent full-model config @db*)
616+
(trigger-auto-compact! chat-ctx tc-all-tools tc-user-messages)
617+
{:tools tc-all-tools
618+
:new-messages (get-in @db* [:chats chat-id :messages])}))))
615619
received-msgs* add-to-history! user-messages)
616620
:on-reason (fn [{:keys [status id text external-id delta-reasoning? redacted? data]}]
617621
(lifecycle/assert-chat-not-stopped! chat-ctx)

src/eca/features/tools/chat.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
;; Save summary to replace chat history later
1313
(swap! db* assoc-in [:chats chat-id :last-summary] summary)
1414

15+
;; Signal that compact is done so the LLM loop stops
16+
(swap! db* assoc-in [:chats chat-id :compact-done?] true)
17+
1518
(tools.util/single-text-content "Compacted successfully!")))
1619

1720
(def definitions

test/eca/features/tools/chat_test.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
(is (= false (:compacting? chat-state))
2929
"Should set compacting? to false")
3030
(is (= test-summary (:last-summary chat-state))
31-
"Should save the summary as last-summary"))))))
31+
"Should save the summary as last-summary")
32+
(is (= true (:compact-done? chat-state))
33+
"Should set compact-done? to true"))))))
3234

3335
(testing "Handles empty summary"
3436
(let [db* (h/db*)
@@ -45,7 +47,8 @@
4547

4648
(let [chat-state (get-in @db* [:chats chat-id])]
4749
(is (= false (:compacting? chat-state)))
48-
(is (= empty-summary (:last-summary chat-state))))))))
50+
(is (= empty-summary (:last-summary chat-state)))
51+
(is (= true (:compact-done? chat-state))))))))
4952

5053
(deftest compact-chat-enabled-test
5154
(testing "Tool is enabled when chat is compacting"

0 commit comments

Comments
 (0)