branch: externals/ellama
commit 8326b43b74a245b930ac49ea16e7b04d3f38466b
Author: Sergey Kostyaev <[email protected]>
Commit: Sergey Kostyaev <[email protected]>
Keep subagent loops running after tool errors
Convert synchronous and async subagent tool exceptions into actionable tool
results so the worker receives the failure and can continue instead of aborting
the provider tool-use path. Add focused tests for sync file errors and async
scheduling errors while preserving loop-detection history.
---
ellama-tools.el | 43 +++++++++++++++++++++++-------
tests/test-ellama-tools.el | 66 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 99 insertions(+), 10 deletions(-)
diff --git a/ellama-tools.el b/ellama-tools.el
index 64dcc2c06d..30fe6b27c2 100644
--- a/ellama-tools.el
+++ b/ellama-tools.el
@@ -4536,6 +4536,15 @@ RESULT for the agent."
session :tool-loop-state loop-state)))
replacement)))
+(defun ellama-tools--subagent-tool-error-result (name err)
+ "Return a tool result for sub-agent tool NAME error ERR."
+ (format
+ "Tool `%s` failed: %s\n\nNext action: choose a different tool call or \
+different arguments. If no different useful action exists, call \
+`report_result` with the blocker."
+ name
+ (error-message-string err)))
+
(defun ellama-tools--wrap-subagent-tool (tool session)
"Return TOOL wrapped with sub-agent loop detection for SESSION."
(let* ((wrapped-tool (copy-sequence tool))
@@ -4548,16 +4557,30 @@ RESULT for the agent."
(if (and async args (functionp (car args)))
(let ((callback (car args))
(call-args (cdr args)))
- (apply
- function
- (lambda (result)
- (funcall
- callback
- (or (ellama-tools--subagent-trace-tool-call
- session name call-args result)
- result)))
- call-args))
- (let ((result (apply function args)))
+ (condition-case err
+ (apply
+ function
+ (lambda (result)
+ (funcall
+ callback
+ (or (ellama-tools--subagent-trace-tool-call
+ session name call-args result)
+ result)))
+ call-args)
+ (error
+ (let ((result
+ (ellama-tools--subagent-tool-error-result name err)))
+ (funcall
+ callback
+ (or (ellama-tools--subagent-trace-tool-call
+ session name call-args result)
+ result))
+ nil))))
+ (let ((result
+ (condition-case err
+ (apply function args)
+ (error
+ (ellama-tools--subagent-tool-error-result name err)))))
(or (ellama-tools--subagent-trace-tool-call
session name args result)
result)))))
diff --git a/tests/test-ellama-tools.el b/tests/test-ellama-tools.el
index b1f178d6d1..a2ab73e0d6 100644
--- a/tests/test-ellama-tools.el
+++ b/tests/test-ellama-tools.el
@@ -3663,6 +3663,72 @@ END_ELLAMA_AGENT_STATE"))
:loop-recovery-count)
2)))))
+(ert-deftest test-ellama-subagent-tool-error-continues-as-result ()
+ (ellama-test--ensure-local-ellama-tools)
+ (let* ((session
+ (make-ellama-session
+ :id "worker-tool-error"
+ :extra (list :tool-loop-state
+ (ellama-tools--subagent-loop-state))))
+ (tool
+ (llm-make-tool
+ :name "directory_tree"
+ :function (lambda (&rest _args)
+ (signal
+ 'file-error
+ '("Opening directory" "Operation not permitted"
+ "/blocked")))))
+ (wrapped (car (ellama-tools--wrap-subagent-tools
+ (list tool) session)))
+ (function (llm-tool-function wrapped))
+ (ellama-tools-subagent-loop-detection-enabled t))
+ (let ((result (funcall function "/parent")))
+ (should (stringp result))
+ (should (string-match-p "Tool `directory_tree` failed" result))
+ (should (string-match-p "Operation not permitted" result))
+ (should (string-match-p "choose a different tool call" result)))
+ (let* ((extra (ellama-session-extra session))
+ (loop-state (plist-get extra :tool-loop-state))
+ (history (plist-get loop-state :tool-history)))
+ (should (equal (plist-get (car history) :name)
+ "directory_tree"))
+ (should (equal (plist-get (car history) :args)
+ '("/parent"))))))
+
+(ert-deftest test-ellama-subagent-async-tool-error-continues-as-result ()
+ (ellama-test--ensure-local-ellama-tools)
+ (let* ((callback-result nil)
+ (session
+ (make-ellama-session
+ :id "worker-async-tool-error"
+ :extra (list :tool-loop-state
+ (ellama-tools--subagent-loop-state))))
+ (tool
+ (llm-make-tool
+ :name "write_file"
+ :async t
+ :function (lambda (&rest _args)
+ (error "Cannot schedule async tool"))))
+ (wrapped (car (ellama-tools--wrap-subagent-tools
+ (list tool) session)))
+ (function (llm-tool-function wrapped))
+ (ellama-tools-subagent-loop-detection-enabled t))
+ (should-not (funcall function
+ (lambda (result)
+ (setq callback-result result))
+ "file.el"
+ "content"))
+ (should (stringp callback-result))
+ (should (string-match-p "Tool `write_file` failed" callback-result))
+ (should (string-match-p "Cannot schedule async tool" callback-result))
+ (let* ((extra (ellama-session-extra session))
+ (loop-state (plist-get extra :tool-loop-state))
+ (history (plist-get loop-state :tool-history)))
+ (should (equal (plist-get (car history) :name)
+ "write_file"))
+ (should (equal (plist-get (car history) :args)
+ '("file.el" "content"))))))
+
(ert-deftest test-ellama-tools-task-tool-role-fallback-and-report-priority ()
(ellama-test--ensure-local-ellama-tools)
(let ((ellama--current-session-id "parent-1")