Skip to content

Commit 58d522f

Browse files
authored
Merge pull request #565 from editor-code-assistant/fix/compact-tool-cache-invalidation
Keep compact tool schema stable accross compaction
2 parents 688a354 + 4c435b0 commit 58d522f

5 files changed

Lines changed: 96 additions & 47 deletions

File tree

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+
- Keep `compact_chat` in the tool schema across normal and compact requests, preserving prompt-cache prefixes while rejecting calls outside active compaction.
6+
57
## 0.152.0
68

79
- Share chat history across git worktrees of the same repo, and merge workspace cache writes from concurrent servers instead of overwriting. #558

src/eca/features/tools/chat.clj

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,25 @@
55
(set! *warn-on-reflection* true)
66

77
(defn ^:private compact-chat [arguments {:keys [db* chat-id]}]
8-
(let [summary (get arguments "summary")]
9-
;; Mark chat as not compacting anymore
10-
(swap! db* assoc-in [:chats chat-id :compacting?] false)
11-
12-
;; Save summary to replace chat history later
13-
(swap! db* assoc-in [:chats chat-id :last-summary] summary)
14-
15-
;; Signal that compact is done so the LLM loop stops
16-
(swap! db* assoc-in [:chats chat-id :compact-done?] true)
17-
18-
(tools.util/single-text-content "Compacted successfully!")))
8+
(let [chat (get-in @db* [:chats chat-id])]
9+
(if (or (:compacting? chat) (:auto-compacting? chat))
10+
(do
11+
(swap! db* update-in [:chats chat-id]
12+
assoc
13+
:compacting? false
14+
:last-summary (get arguments "summary")
15+
:compact-done? true)
16+
(tools.util/single-text-content "Compacted successfully!"))
17+
(tools.util/single-text-content
18+
"Chat compaction is not active for this request. This tool is available only while chat compaction is in progress. To compact manually, the user must use the `/compact` command; compaction may also start automatically when context usage reaches the configured threshold."
19+
:error))))
1920

2021
(def definitions
2122
{"compact_chat"
22-
{:description "Compact / summarize a chat, cleaning chat history, emptying usage and presenting the summary to user"
23+
{:description "During chat compaction, submit a summary that will become the active conversation context"
2324
:parameters {:type "object"
2425
:properties {"summary" {:type "string"
2526
:description "The summary/compacted text"}}
2627
:required ["summary"]}
2728
:handler #'compact-chat
28-
:enabled-fn (fn [{:keys [db chat-id]}]
29-
(or (get-in db [:chats chat-id :compacting?] false)
30-
(get-in db [:chats chat-id :auto-compacting?] false)))
3129
:summary-fn (constantly "Compacting...")}})

test/eca/features/chat_test.clj

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,47 @@
12381238
{:role :system :content {:state :finished :type :progress}}])}
12391239
(h/messages))))))
12401240

1241+
(deftest inactive-compact-tool-error-reaches-provider-continuation-test
1242+
(testing "inactive compact_chat result is included in the next provider request"
1243+
(h/reset-components!)
1244+
(let [continuation* (atom nil)
1245+
original-all-tools f.tools/all-tools
1246+
original-call-tool! f.tools/call-tool!
1247+
expected-error "Chat compaction is not active for this request. This tool is available only while chat compaction is in progress. To compact manually, the user must use the `/compact` command; compaction may also start automatically when context usage reaches the configured threshold."
1248+
{:keys [chat-id]}
1249+
(prompt!
1250+
{:message "Continue normally"}
1251+
{:all-tools-mock original-all-tools
1252+
:call-tool-mock original-call-tool!
1253+
:api-mock
1254+
(fn [{:keys [on-first-response-received on-message-received
1255+
on-prepare-tool-call on-tools-called]}]
1256+
(on-first-response-received)
1257+
(on-prepare-tool-call {:id "compact-call-1"
1258+
:full-name "eca__compact_chat"
1259+
:arguments-text "{\"summary\":\"Must not be stored\"}"})
1260+
(reset! continuation*
1261+
(on-tools-called [{:id "compact-call-1"
1262+
:full-name "eca__compact_chat"
1263+
:arguments {"summary" "Must not be stored"}}]))
1264+
(on-message-received {:type :text :text "Understood"})
1265+
(on-message-received {:type :finish}))})
1266+
tool-output (->> (:new-messages @continuation*)
1267+
(filter #(= "tool_call_output" (:role %)))
1268+
last
1269+
:content)]
1270+
(is (match? {:id "compact-call-1"
1271+
:full-name "eca__compact_chat"
1272+
:error true
1273+
:output {:error true
1274+
:contents [{:type :text :text expected-error}]}}
1275+
tool-output)
1276+
"the provider continuation must contain the exact inactive-compaction error for the LLM")
1277+
(is (nil? (get-in (h/db) [:chats chat-id :last-summary])))
1278+
(is (nil? (get-in (h/db) [:chats chat-id :compact-done?])))
1279+
(is (not (true? (get-in (h/db) [:chats chat-id :compacting?])))
1280+
"the accidental call must not activate or complete compaction"))))
1281+
12411282
(deftest concurrent-tool-calls-test
12421283
(testing "Running three calls simultaneously"
12431284
(h/reset-components!)

test/eca/features/tools/chat_test.clj

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@
3232
(is (= true (:compact-done? chat-state))
3333
"Should set compact-done? to true"))))))
3434

35+
(testing "Successfully compacts an auto-compacting chat"
36+
(let [db* (h/db*)
37+
chat-id "test-chat-auto-compacting"
38+
test-summary "Auto-compacted summary"]
39+
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
40+
(let [result ((get-in f.tools.chat/definitions ["compact_chat" :handler])
41+
{"summary" test-summary}
42+
{:db* db* :chat-id chat-id})
43+
chat-state (get-in @db* [:chats chat-id])]
44+
(is (false? (:error result)))
45+
(is (= test-summary (:last-summary chat-state)))
46+
(is (true? (:compact-done? chat-state))))))
47+
3548
(testing "Handles empty summary"
3649
(let [db* (h/db*)
3750
chat-id "test-chat-456"
@@ -50,37 +63,20 @@
5063
(is (= empty-summary (:last-summary chat-state)))
5164
(is (= true (:compact-done? chat-state))))))))
5265

53-
(deftest compact-chat-enabled-test
54-
(testing "Tool is enabled when chat is compacting"
55-
(let [db* (h/db*)
56-
chat-id "test-chat-compacting"]
57-
(swap! db* assoc-in [:chats chat-id :compacting?] true)
58-
59-
(is (true? ((get-in f.tools.chat/definitions ["compact_chat" :enabled-fn])
60-
{:db @db* :chat-id chat-id})))))
61-
62-
(testing "Tool is disabled when chat is not compacting"
63-
(let [db* (h/db*)
64-
chat-id "test-chat-not-compacting"]
65-
(swap! db* assoc-in [:chats chat-id :compacting?] false)
66-
67-
(is (false? ((get-in f.tools.chat/definitions ["compact_chat" :enabled-fn])
68-
{:db @db* :chat-id chat-id})))))
69-
70-
(testing "Tool is disabled when compacting? is not set (defaults to false)"
71-
(let [db* (h/db*)
72-
chat-id "test-chat-no-compacting-key"]
73-
;; Don't set compacting? at all
74-
75-
(is (false? ((get-in f.tools.chat/definitions ["compact_chat" :enabled-fn])
76-
{:db @db* :chat-id chat-id})))))
77-
78-
(testing "Tool is disabled when chat doesn't exist"
79-
(let [db* (h/db*)
80-
chat-id "non-existent-chat"]
81-
82-
(is (false? ((get-in f.tools.chat/definitions ["compact_chat" :enabled-fn])
83-
{:db @db* :chat-id chat-id}))))))
66+
(deftest compact-chat-requires-active-compaction-test
67+
(let [db* (h/db*)
68+
chat-id "test-chat-not-compacting"
69+
handler (get-in f.tools.chat/definitions ["compact_chat" :handler])]
70+
(swap! db* assoc-in [:chats chat-id] {:id chat-id})
71+
(let [before @db*
72+
result (handler {"summary" "Must not be stored"}
73+
{:db* db* :chat-id chat-id})]
74+
(is (match? {:error true
75+
:contents [{:type :text
76+
:text "Chat compaction is not active for this request. This tool is available only while chat compaction is in progress. To compact manually, the user must use the `/compact` command; compaction may also start automatically when context usage reaches the configured threshold."}]}
77+
result))
78+
(is (= before @db*)
79+
"Inactive compact tool calls must not mutate chat state"))))
8480

8581
(deftest compact-chat-summary-fn-test
8682
(testing "Summary function returns constant string"
@@ -93,7 +89,8 @@
9389
(is (string? (:description tool-def)) "Should have a description")
9490
(is (map? (:parameters tool-def)) "Should have parameters")
9591
(is (or (fn? (:handler tool-def)) (var? (:handler tool-def))) "Should have a handler function or var")
96-
(is (fn? (:enabled-fn tool-def)) "Should have an enabled-fn")
92+
(is (not (contains? tool-def :enabled-fn))
93+
"Tool availability must not change the provider tool schema")
9794
(is (fn? (:summary-fn tool-def)) "Should have a summary-fn")))
9895

9996
(testing "Tool parameters schema is correct"

test/eca/features/tools_test.clj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@
3838
:origin :native}])
3939
(f.tools/all-tools "123" "code" {} {}))))
4040

41+
(testing "Compact tool schema is stable across compaction state"
42+
(let [db {:chats {"123" {}}}
43+
wire-tools (fn [db]
44+
(mapv #(select-keys % [:full-name :description :parameters])
45+
(f.tools/all-tools "123" "code" db {})))
46+
normal-tools (wire-tools db)
47+
manual-tools (wire-tools (assoc-in db [:chats "123" :compacting?] true))
48+
auto-tools (wire-tools (assoc-in db [:chats "123" :auto-compacting?] true))]
49+
(is (some #(= "eca__compact_chat" (:full-name %)) normal-tools))
50+
(is (= normal-tools manual-tools auto-tools))))
51+
4152
(testing "Subagent excludes spawn_agent, task, git, and ask_user tools"
4253
(let [db {:chats {"sub-1" {:subagent {:name "explorer"}}}}
4354
tools (f.tools/all-tools "sub-1" "code" db {})

0 commit comments

Comments
 (0)