branch: externals/ellama
commit 8ff96673a2cc8099082310fbbb8263feba04e071
Author: Sergey Kostyaev <[email protected]>
Commit: Sergey Kostyaev <[email protected]>

    Preflight compact session requests
    
    Added pre-request automatic compaction based on estimated prompt size so 
controller continuation turns compact before sending an oversized request. 
Covered the generic ellama-stream path and plan-and-act controller continuation 
with regression tests.
---
 ellama.el                  | 37 ++++++++++++++++++++-
 tests/test-ellama-tools.el | 83 ++++++++++++++++++++++++++++++++++++++++++++++
 tests/test-ellama.el       | 49 +++++++++++++++++++++++++++
 3 files changed, 168 insertions(+), 1 deletion(-)

diff --git a/ellama.el b/ellama.el
index db570a00e8..14772d7177 100644
--- a/ellama.el
+++ b/ellama.el
@@ -1888,6 +1888,32 @@ REQUEST-CONTEXT is the active request context."
      :on-done on-done
      :request-context request-context)))
 
+(cl-defun ellama--session-auto-compact-before-request-maybe
+    (session provider prompt buffer &key on-done request-context)
+  "Compact SESSION before sending PROMPT when estimated context is too large.
+PROVIDER is the session provider.  BUFFER is the chat buffer.
+ON-DONE is called after asynchronous compaction succeeds or fails.
+REQUEST-CONTEXT is the active request context."
+  (when (and ellama-session-auto-compact-enabled
+             (ellama-session-p session)
+             (llm-chat-prompt-p prompt)
+             (not (ellama--session-extra-get
+                   session :auto-compact-in-progress)))
+    (when-let* ((token-count
+                 (ellama--session-compact-estimate-prompt-tokens
+                  provider prompt))
+                (threshold
+                 (ellama--session-auto-compact-threshold provider))
+                ((>= token-count threshold)))
+      (ellama--session-compact
+       session
+       :provider provider
+       :buffer buffer
+       :token-count token-count
+       :automatic t
+       :on-done on-done
+       :request-context request-context))))
+
 (defun ellama--active-session-by-id (id)
   "Return active session matching display ID."
   (catch 'session
@@ -3604,7 +3630,16 @@ failure (with BUFFER current).
                           request-context))
                    (ellama--set-session-request-context
                     session request-context)))))
-          (start-request))))))
+          (unless
+              (ellama--session-auto-compact-before-request-maybe
+               session provider llm-prompt buffer
+               :request-context request-context
+               :on-done
+               (lambda ()
+                 (when (ellama-session-p session)
+                   (setq llm-prompt (ellama-session-prompt session)))
+                 (start-request)))
+            (start-request)))))))
 
 (defun ellama-chain (initial-prompt forms &optional acc)
   "Call chain of FORMS on INITIAL-PROMPT.
diff --git a/tests/test-ellama-tools.el b/tests/test-ellama-tools.el
index 71337a77fd..8ef8adbbd1 100644
--- a/tests/test-ellama-tools.el
+++ b/tests/test-ellama-tools.el
@@ -3235,6 +3235,89 @@ END_ELLAMA_AGENT_STATE"))
       (when (buffer-live-p buffer)
         (kill-buffer buffer)))))
 
+(ert-deftest 
test-ellama-agent-loop-handler-compacts-before-controller-continue ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((buffer (generate-new-buffer " *ellama-agent-compact-test*"))
+         (provider (make-llm-fake))
+         (prompt (llm-make-chat-prompt "user 1" :context "System"))
+         (base-tool (llm-make-tool :name "read_file" :function #'ignore))
+         (session
+          (make-ellama-session
+           :id "agent-compact"
+           :provider provider
+           :prompt prompt
+           :extra (list :uid "agent-compact-uid"
+                        :tools (list base-tool)
+                        :agent-loop
+                        (list :phase 'acting
+                              :plan (list (list :id 1
+                                                :title "Keep going"
+                                                :status 'pending))
+                              :step-count 0
+                              :max-steps 3
+                              :completed nil
+                              :system "System"))))
+         (ellama-response-process-method 'async)
+         (ellama-spinner-enabled nil)
+         (ellama-session-auto-compact-enabled t)
+         (ellama-session-auto-compact-token-threshold 100)
+         (ellama-session-auto-compact-provider (make-llm-fake))
+         (ellama-session-auto-compact-keep-last-turns 2)
+         (ellama-session-auto-compact-show-message nil)
+         (ellama-session-hide-org-quotes nil)
+         (call-count 0)
+         compact-callback
+         main-prompt)
+    (llm-chat-prompt-append-response prompt "assistant 1" 'assistant)
+    (llm-chat-prompt-append-response prompt "user 2")
+    (llm-chat-prompt-append-response prompt "assistant 2" 'assistant)
+    (llm-chat-prompt-append-response prompt "user 3")
+    (llm-chat-prompt-append-response prompt "assistant 3" 'assistant)
+    (llm-chat-prompt-append-response prompt "user 4")
+    (llm-chat-prompt-append-response prompt "assistant 4" 'assistant)
+    (unwind-protect
+        (progn
+          (with-current-buffer buffer
+            (org-mode)
+            (setq-local ellama--current-session session))
+          (cl-letf (((symbol-function 'ellama-get-session-buffer)
+                     (lambda (id)
+                       (and (member id '("agent-compact"
+                                         "agent-compact-uid"))
+                            buffer)))
+                    ((symbol-function 'llm-count-tokens)
+                     (lambda (_provider _text) 120))
+                    ((symbol-function 'llm-chat-async)
+                     (lambda (_provider sent-prompt response-callback
+                                        _error-callback
+                                        &optional _multi-output)
+                       (setq call-count (1+ call-count))
+                       (if (= call-count 1)
+                           (progn
+                             (setq compact-callback response-callback)
+                             'compact-request)
+                         (setq main-prompt sent-prompt)
+                         'main-request))))
+            (funcall
+             (ellama-tools--make-agent-loop-handler session buffer "System")
+             "Previous response")
+            (should (= call-count 1))
+            (should compact-callback)
+            (should-not main-prompt)
+            (should (ellama--session-extra-get
+                     session :auto-compact-in-progress))
+            (funcall compact-callback '(:text "Summary"))
+            (should (= call-count 2))
+            (should (eq main-prompt (ellama-session-prompt session)))
+            (should (string-match-p
+                     "Current plan state"
+                     (llm-chat-prompt-to-text main-prompt)))
+            (should (string-match-p
+                     "Next pending item: Keep going"
+                     (llm-chat-prompt-to-text main-prompt)))))
+      (when (buffer-live-p buffer)
+        (kill-buffer buffer)))))
+
 (ert-deftest test-ellama-agent-report-result-restores-original-tools ()
   (ellama-test--ensure-local-ellama-tools)
   (let* ((buffer (generate-new-buffer " *ellama-agent-done-test*"))
diff --git a/tests/test-ellama.el b/tests/test-ellama.el
index 68c30107f0..26852faf99 100644
--- a/tests/test-ellama.el
+++ b/tests/test-ellama.el
@@ -1457,6 +1457,55 @@ detailed comparison to help you decide:
 (ert-deftest test-ellama-session-auto-compact-enabled-by-default ()
   (should ellama-session-auto-compact-enabled))
 
+(ert-deftest test-ellama-stream-preflight-auto-compacts-before-request ()
+  (let* ((provider (make-llm-fake))
+         (session (ellama-test--compact-session provider))
+         (ellama-response-process-method 'async)
+         (ellama-spinner-enabled nil)
+         (ellama-session-auto-compact-enabled t)
+         (ellama-session-auto-compact-token-threshold 100)
+         (ellama-session-auto-compact-provider (make-llm-fake))
+         (ellama-session-auto-compact-keep-last-turns 2)
+         (ellama-session-auto-compact-show-message nil)
+         (ellama-session-hide-org-quotes nil)
+         (call-count 0)
+         compact-callback
+         main-prompt
+         done-text)
+    (cl-letf (((symbol-function 'llm-count-tokens)
+               (lambda (_provider _text) 120))
+              ((symbol-function 'llm-chat-async)
+               (lambda (_provider prompt response-callback
+                                  _error-callback &optional _multi-output)
+                 (setq call-count (1+ call-count))
+                 (if (= call-count 1)
+                     (progn
+                       (setq compact-callback response-callback)
+                       'compact-request)
+                   (setq main-prompt prompt)
+                   'main-request))))
+      (with-temp-buffer
+        (setq-local ellama--current-session session)
+        (ellama-stream "continue after compaction"
+                       :session session
+                       :on-done (lambda (text)
+                                  (setq done-text text)))
+        (should (= call-count 1))
+        (should compact-callback)
+        (should (ellama--session-extra-get
+                 session :auto-compact-in-progress))
+        (should-not main-prompt)
+        (should-not done-text)
+        (funcall compact-callback '(:text "Summary"))
+        (should (= call-count 2))
+        (should-not (ellama--session-extra-get
+                     session :auto-compact-in-progress))
+        (should (eq ellama--current-request 'main-request))
+        (should (eq main-prompt (ellama-session-prompt session)))
+        (should (string-match-p
+                 "continue after compaction"
+                 (llm-chat-prompt-to-text main-prompt)))))))
+
 (ert-deftest test-ellama-session-auto-compact-needed-above-threshold ()
   (let* ((provider (make-llm-fake))
          (session (ellama-test--compact-session provider))

Reply via email to