diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0862353..4a2a2f527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Auto-clear completed task lists: when a new prompt is sent and all tasks are done, the list is automatically cleared. + ## 0.142.2 - `/context` now shows where auto-compaction triggers: a `🔲` marker on the threshold cell of the grid plus an `Auto-compaction at N%` line. diff --git a/resources/prompts/tools/task.md b/resources/prompts/tools/task.md index 3d1dad0a3..db9661b16 100644 --- a/resources/prompts/tools/task.md +++ b/resources/prompts/tools/task.md @@ -36,4 +36,3 @@ Workflow & Strict Execution Rules: 4. SUBAGENTS & PARALLEL WORK: For sequential work, keep only one task `in_progress`; later tasks must stay pending until you start them. Multiple tasks may be `in_progress` only for concurrent separate workstreams (e.g. separate subagents). Only the main agent updates the task list. 5. COMPLETION TIMING: Once a task is verified, close it as soon as possible. Do not delay completion just to align with other tasks. 6. ADAPTABILITY: If completing tasks reveals follow-up work, use 'add' to append new tasks. -7. CLEANUP: When all tasks are done and no further work remains, use 'clear'. diff --git a/src/eca/features/chat.clj b/src/eca/features/chat.clj index 4a6f7faed..323491df7 100644 --- a/src/eca/features/chat.clj +++ b/src/eca/features/chat.clj @@ -21,6 +21,7 @@ [eca.features.skills :as f.skills] [eca.features.tools :as f.tools] [eca.features.tools.mcp :as f.mcp] + [eca.features.tools.task :as f.tools.task] [eca.llm-api :as llm-api] [eca.llm-providers.errors :as llm-providers.errors] [eca.llm-util :as llm-util] @@ -1653,6 +1654,13 @@ _ (when (and seeded-default-trust? provided-chat-id) (config/notify-fields-changed-only! {:chat {:select-trust true}} messenger db* chat-id))] (logger/with-chat-context chat-id (:parent-chat-id base-chat-ctx) + (when-let [cleared-details (f.tools.task/auto-clear-completed! db* chat-id)] + (logger/info logger-tag "Auto-cleared completed task list" {:chat-id chat-id}) + (lifecycle/send-content! base-chat-ctx :assistant + {:type :toolCalled + :server "eca" + :name "task" + :details cleared-details})) (try (prompt* params base-chat-ctx) (catch Exception e diff --git a/src/eca/features/tools/task.clj b/src/eca/features/tools/task.clj index 4eae27156..8f06f066a 100644 --- a/src/eca/features/tools/task.clj +++ b/src/eca/features/tools/task.clj @@ -256,6 +256,23 @@ [_name _arguments before-details result _ctx] (or (:details result) before-details)) +;; --- Auto-clear --- + +(defn ^:private all-tasks-done? + "Returns true if the task list has tasks and all are :done." + [state] + (let [tasks (:tasks state) + {:keys [done pending in-progress]} (status-counts tasks)] + (and (pos? done) (zero? pending) (zero? in-progress)))) + +(defn auto-clear-completed! + "Clear the task list when all tasks are done. + Returns task-details of the cleared state if clearing occurred, nil otherwise." + [db* chat-id] + (when (all-tasks-done? (get-task @db* chat-id)) + (mutate-task! db* chat-id (fn [_] {:state empty-task})) + (task-details empty-task))) + ;; --- Operations --- (defn ^:private op-read [_arguments {:keys [db chat-id]}] @@ -502,7 +519,7 @@ :items {:type "integer"} :description "Task IDs (required for start/complete/delete)"} :active_summary {:type "string" - :description "Summary of what will be done in the current active session. Required for start operation."} + :description "Summary of what will be done in the current active session. Required for start operation."} :task {:type "object" :description "Single task data (for add/update)" :properties {:subject {:type "string" :description "Task subject/title (required)"} diff --git a/test/eca/features/tools/task_test.clj b/test/eca/features/tools/task_test.clj index 1f2fcced5..3df77f69d 100644 --- a/test/eca/features/tools/task_test.clj +++ b/test/eca/features/tools/task_test.clj @@ -482,3 +482,48 @@ result nil)] (is (= (:details result) details))))) + +(deftest auto-clear-completed-test + (testing "clears task list when all tasks are done" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary nil + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :done :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (some? result)) + (is (= :task (:type result))) + (is (empty? (:tasks result))) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))) + + (testing "does not clear when tasks are still pending" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary nil + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :pending :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= 2 (count (:tasks (task/get-task @db* "c1"))))))) + + (testing "does not clear when tasks are in progress" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary "working" + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :in-progress :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= 2 (count (:tasks (task/get-task @db* "c1"))))))) + + (testing "does nothing when task list is empty" + (let [db* (atom {:chats {"c1" {:task {:next-id 1 :active-summary nil :tasks []}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))) + + (testing "does nothing when chat has no task list" + (let [db* (atom {}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))))