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))