branch: externals/ellama
commit b00c307c97a2e447948a589fe591e3cad2d4b93a
Merge: 09857e5d2ec 50cc3903b88
Author: Sergey Kostyaev <[email protected]>
Commit: GitHub <[email protected]>

    Merge pull request #435 from s-kostyaev/feature/split-task-tools
    
    Split subagent task tool contracts
---
 NEWS.org                       |  12 ++
 README.org                     |  50 +++++---
 ellama-tools.el                | 271 +++++++++++++++++++++++++----------------
 ellama-transient.el            |  11 +-
 ellama.el                      |   2 +-
 ellama.info                    | 131 +++++++++++---------
 tests/test-ellama-tools.el     | 129 +++++++++++++++++---
 tests/test-ellama-transient.el |  22 ++++
 8 files changed, 434 insertions(+), 194 deletions(-)

diff --git a/NEWS.org b/NEWS.org
index 21940a492a5..810aa7ce634 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -1,3 +1,15 @@
+* Version 1.30.0
+- Split the subagent task tool into two separate contracts. The ~task~ tool
+  handles free-form delegation, while ~task_from_template~ handles
+  template-based delegation. Both now have proper required schemas and
+  validation checks instead of accepting anything silently. The direct Lisp
+  template interface still works as before. Updated the docs and added
+  regression tests.
+- Added a plan-and-act action to the blueprint-mode transient menu. Starting it
+  kicks off the plan-and-act agent loop from the current blueprint buffer and
+  then closes the buffer. Included regression tests to verify the buffer
+  contents get forwarded to ~ellama-plan-and-act~ and that the source buffer is
+  killed once the loop starts.
 * Version 1.29.0
 
 - Added ~ellama-setup-agentic-coding~ to configure long-running coding-agent
diff --git a/README.org b/README.org
index c28288c9721..691f7b6c3de 100644
--- a/README.org
+++ b/README.org
@@ -624,12 +624,13 @@ generated text string.
 
 *** Task Templates Blueprints and Skills
 
-- ~ellama-tools-task-template-dirs~: Directories where the ~task~ tool searches
-  for prompt templates when no ~template_base~ is supplied. Relative template
-  names are resolved below these directories.
+- ~ellama-tools-task-template-dirs~: Directories where direct
+  ~ellama-tools-task-from-template-tool~ calls search for prompt templates when
+  no ~template_base~ is supplied. Relative template names are resolved below
+  these directories.
 - ~ellama-tools-task-template-allow-absolute-paths~: Allow absolute file names
-  in the ~task~ tool ~template~ argument. Disabled by default. Relative 
template
-  names are still supported.
+  in the ~task_from_template~ tool ~template~ argument. Disabled by default.
+  Relative template names are still supported.
 - ~ellama-blueprint-global-dir~: Global directory for storing blueprint files.
 - ~ellama-blueprint-local-dir~: Local directory name for project-specific
   blueprints.
@@ -912,12 +913,16 @@ to decide whether it is ephemeral or persistent.
 
 ** Task Tool Subagents
 
-The ~task~ tool delegates work to an asynchronous sub-agent. The parent model
-gets control back immediately, and the sub-agent reports the final result by
-calling the injected ~report_result~ tool. If the sub-agent finishes a turn
-without reporting a result, Ellama sends 
~ellama-tools-subagent-continue-prompt~
-until the task is complete or ~ellama-tools-subagent-default-max-steps~ is
-reached.
+Ellama provides two tools for delegating work to an asynchronous sub-agent.  
Use
+~task~ with a free-form prompt and ~task_from_template~ with a prompt
+template. Keeping these call shapes separate makes both arguments and 
validation
+errors easier for local models to follow.
+
+Both tools return control to the parent model immediately. The sub-agent 
reports
+its final result by calling the injected ~report_result~ tool. If it finishes a
+turn without reporting a result, Ellama sends
+~ellama-tools-subagent-continue-prompt~ until the task is complete or
+~ellama-tools-subagent-default-max-steps~ is reached.
 
 Each sub-agent runs in its own Ellama session. When
 ~ellama-display-session-buffer-on-generation~ is non-nil, Ellama displays both
@@ -942,7 +947,16 @@ error resets the counter, compacts the sub-agent session 
automatically, and
 continues the loop after compaction finishes or is skipped. Any successful
 sub-agent response resets the consecutive error counter.
 
-The tool accepts either a free-form ~description~ or a prompt template:
+The ~task~ tool requires a non-empty ~description~ and a configured ~role~:
+
+#+begin_src json
+{
+  "description": "Inspect the parser and report possible edge cases.",
+  "role": "explorer"
+}
+#+end_src
+
+Use ~task_from_template~ when a skill provides a reusable prompt:
 
 #+begin_src json
 {
@@ -958,13 +972,19 @@ The tool accepts either a free-form ~description~ or a 
prompt template:
 
 Prompt templates are plain text files with placeholders like ~{main_topic}~. 
The
 ~arguments~ object must provide values whose keys match the placeholder
-names. If validation fails, the tool returns an error with hints listing 
missing
+names. All four ~task_from_template~ arguments are required. Skills that use
+this tool should include the exact call shape and use their own directory as
+~template_base~. If validation fails, the tool returns an error with the 
missing
 and unused keys and an example object to retry.
 
+Skills written for the older combined ~task~ interface should change the tool
+name to ~task_from_template~. Their template arguments stay the same.
+
 Template resolution is intentionally scoped:
 
-- relative ~template~ paths are resolved under ~template_base~ when supplied
-- otherwise relative paths are searched under ~ellama-tools-task-template-dirs~
+- relative ~template~ paths are resolved under ~template_base~
+- direct Lisp calls can omit ~template_base~ and search
+  ~ellama-tools-task-template-dirs~
 - path traversal outside the selected base directory is rejected
 - absolute template paths are rejected unless
   ~ellama-tools-task-template-allow-absolute-paths~ is non-nil
diff --git a/ellama-tools.el b/ellama-tools.el
index c0521ffc1ab..4205ab117c2 100644
--- a/ellama-tools.el
+++ b/ellama-tools.el
@@ -321,14 +321,14 @@ Values below 2 are treated as 2 so the first call is 
never considered a loop."
 (defcustom ellama-tools-task-template-dirs
   (list (locate-user-emacs-file "ellama/task-templates"))
   "Directories used to resolve relative task template names.
-The `task' tool can render a template before spawning a sub-agent.  When
-`template_base' is omitted in the tool call, relative template names are
-searched in these directories."
+The `task_from_template' tool renders a template before spawning a sub-agent.
+When `template_base' is omitted in a direct Lisp call, relative template names
+are searched in these directories."
   :type '(repeat directory)
   :group 'ellama)
 
 (defcustom ellama-tools-task-template-allow-absolute-paths nil
-  "Allow `task' template names to be absolute file names.
+  "Allow `task_from_template' template names to be absolute file names.
 When nil, absolute template names are rejected.  Relative template names are
 always resolved under either the tool call's `template_base' argument or
 `ellama-tools-task-template-dirs'."
@@ -416,15 +416,18 @@ BEGIN_ELLAMA_AGENT_STATE block. When all items are 
complete, call
   :type '(alist :key-type string :value-type plist)
   :group 'ellama)
 
+(defun ellama-tools--subagent-task-tool-p (tool)
+  "Return non-nil when TOOL can spawn a sub-agent."
+  (member (llm-tool-name tool) '("task" "task_from_template")))
+
 (defun ellama-tools--for-role (role)
   "Resolve tools allowed for ROLE."
   (let* ((cfg (cdr (assoc role ellama-tools-subagent-roles)))
          (tools (plist-get cfg :tools)))
     (cond
      ((eq tools :all)
-      (cl-remove-if
-       (lambda (tool) (string= (llm-tool-name tool) "task"))
-       ellama-tools-available))
+      (cl-remove-if #'ellama-tools--subagent-task-tool-p
+                    ellama-tools-available))
      ((listp tools)
       (cl-remove-if-not
        (lambda (tool) (member (llm-tool-name tool) tools))
@@ -3852,7 +3855,7 @@ SUPPLIED, MISSING and UNUSED are lists of argument names."
       "\n\nUnused arguments:\n"
       (ellama-tools--task-template-bullet-list unused)
       "\n\nThese were ignored. Use only the required argument names above."))
-   "\n\nRetry the task call using this shape:\n"
+   "\n\nRetry the task_from_template call using this shape:\n"
    (ellama-tools--task-template-example
     template template-base role required)))
 
@@ -4922,117 +4925,179 @@ and restarts the loop."
            session (plist-put extra :consecutive-error-count new-count))
           (ellama-tools--continue-subagent-after-error session))))))
 
+(defun ellama-tools--task-role (role)
+  "Return ROLE when it names a configured sub-agent role.
+Signal an actionable error for an unknown role."
+  (if (assoc role ellama-tools-subagent-roles)
+      role
+    (error
+     (concat
+      "Task validation failed. No subagent was started.\n\n"
+      "Unknown role: %s\n"
+      "Available roles: %s\n\n"
+      "Retry with one of the available role values.")
+     role
+     (mapconcat #'car ellama-tools-subagent-roles ", "))))
+
+(defun ellama-tools--task-free-form-description (description role)
+  "Return validated free-form task DESCRIPTION.
+ROLE is included in the retry example."
+  (if (and (stringp description)
+           (not (string-empty-p description)))
+      description
+    (error
+     (concat
+      "Task validation failed. No subagent was started.\n\n"
+      "The description must be a non-empty string.\n\n"
+      "Retry the task call using this shape:\n"
+      "{\n  \"description\": \"...\",\n  \"role\": %S\n}")
+     role)))
+
+(defun ellama-tools--start-task (callback description role-key)
+  "Start a sub-agent for DESCRIPTION and call CALLBACK with its result.
+ROLE-KEY must name a configured entry in `ellama-tools-subagent-roles'."
+  (let* ((parent-id ellama--current-session-id)
+
+         (provider (ellama-tools--provider-for-role role-key))
+         (role-cfg   (cdr (assoc role-key ellama-tools-subagent-roles)))
+         (system-msg (plist-get role-cfg :system))
+         (subagent-system
+          (ellama-tools--subagent-system-message system-msg))
+
+         (steps-limit ellama-tools-subagent-default-max-steps)
+
+         ;; ---- create ephemeral worker session ----
+         (worker (ellama-new-session provider description t))
+         (worker-buffer (ellama-get-session-buffer
+                         (ellama-session-id worker)))
+         (worker-point (ellama-tools--insert-subagent-prompt
+                        worker-buffer description))
+
+         ;; ---- resolve tools for role ----
+         (role-tools (ellama-tools--for-role role-key))
+         (subagent-tools
+          (ellama-tools--wrap-subagent-tools role-tools worker))
+
+         ;; ---- dynamic report_result tool ----
+         (report-tool
+          (apply #'llm-make-tool
+                 (ellama-tools--make-report-result-tool
+                  callback worker)))
+
+         ;; IMPORTANT: report tool must be first (termination tool priority)
+         (all-tools (cons report-tool subagent-tools)))
+
+    ;; ============================================================
+    ;; Initialize session state (single source of truth)
+    ;; ============================================================
+
+    (ellama-tools--set-session-extra
+     worker
+     (ellama-tools--session-extra-with
+      worker
+      :parent-session parent-id
+      :role role-key
+      :tools all-tools
+      :result-callback callback
+      :task-completed nil
+      :step-count 0
+      :consecutive-error-count 0
+      :max-steps steps-limit
+      :tool-loop-state (ellama-tools--subagent-loop-state)
+      :system subagent-system))
+
+    ;; ============================================================
+    ;; Start the agent loop
+    ;; ============================================================
+
+    (ellama-stream
+     description
+     :buffer worker-buffer
+     :point worker-point
+     :session worker
+     :on-error (ellama-tools--make-subagent-error-callback worker)
+     :on-done (ellama-tools--make-subagent-loop-handler
+               worker worker-buffer subagent-system)
+     :tools all-tools
+     :system subagent-system)
+
+    ;; ============================================================
+    ;; Immediate response to parent LLM (async contract)
+    ;; ============================================================
+
+    (message "Subtask started (session %s, role %s). Waiting for result via 
callback."
+             (ellama-session-id worker)
+             role-key)
+    nil))
+
 (defun ellama-tools-task-tool
-    (callback &optional description role template template-base arguments)
-  "Delegate DESCRIPTION or rendered TEMPLATE to a sub-agent asynchronously.
-
-CALLBACK   – function called once with the result string.
-ROLE       – role key from `ellama-tools-subagent-roles'.
-TEMPLATE   – optional template name or relative path.
-TEMPLATE-BASE – optional base directory for relative TEMPLATE.
-ARGUMENTS  – object with template substitution values."
-  (let ((role-key (if (assoc role ellama-tools-subagent-roles)
-                      role
-                    "general")))
+    (callback description role &optional template template-base arguments)
+  "Delegate free-form DESCRIPTION to a ROLE sub-agent asynchronously.
+CALLBACK is called once with the result string.
+
+TEMPLATE, TEMPLATE-BASE and ARGUMENTS preserve compatibility for direct Lisp
+callers.  Models only see the strict DESCRIPTION and ROLE tool schema."
+  (if template
+      (ellama-tools-task-from-template-tool
+       callback template template-base arguments role)
     (condition-case err
-        (let* ((description
-                (ellama-tools--task-description
-                 description template template-base role-key arguments))
-               (parent-id ellama--current-session-id)
-
-               (provider (ellama-tools--provider-for-role role-key))
-               (role-cfg   (cdr (assoc role-key ellama-tools-subagent-roles)))
-               (system-msg (plist-get role-cfg :system))
-               (subagent-system
-                (ellama-tools--subagent-system-message system-msg))
-
-               (steps-limit ellama-tools-subagent-default-max-steps)
-
-               ;; ---- create ephemeral worker session ----
-               (worker (ellama-new-session provider description t))
-               (worker-buffer (ellama-get-session-buffer
-                               (ellama-session-id worker)))
-               (worker-point (ellama-tools--insert-subagent-prompt
-                              worker-buffer description))
-
-               ;; ---- resolve tools for role ----
-               (role-tools (ellama-tools--for-role role-key))
-               (subagent-tools
-                (ellama-tools--wrap-subagent-tools role-tools worker))
-
-               ;; ---- dynamic report_result tool ----
-               (report-tool
-                (apply #'llm-make-tool
-                       (ellama-tools--make-report-result-tool
-                        callback worker)))
-
-               ;; IMPORTANT: report tool must be first (termination tool 
priority)
-               (all-tools (cons report-tool subagent-tools)))
-
-          ;; ============================================================
-          ;; Initialize session state (single source of truth)
-          ;; ============================================================
-
-          (ellama-tools--set-session-extra
-           worker
-           (ellama-tools--session-extra-with
-            worker
-            :parent-session parent-id
-            :role role-key
-            :tools all-tools
-            :result-callback callback
-            :task-completed nil
-            :step-count 0
-            :consecutive-error-count 0
-            :max-steps steps-limit
-            :tool-loop-state (ellama-tools--subagent-loop-state)
-            :system subagent-system))
-
-          ;; ============================================================
-          ;; Start the agent loop
-          ;; ============================================================
-
-          (ellama-stream
-           description
-           :buffer worker-buffer
-           :point worker-point
-           :session worker
-           :on-error (ellama-tools--make-subagent-error-callback worker)
-           :on-done (ellama-tools--make-subagent-loop-handler
-                     worker worker-buffer subagent-system)
-           :tools all-tools
-           :system subagent-system)
-
-          ;; ============================================================
-          ;; Immediate response to parent LLM (async contract)
-          ;; ============================================================
-
-          (message "Subtask started (session %s, role %s). Waiting for result 
via callback."
-                   (ellama-session-id worker)
-                   role-key)
-          nil)
+        (let ((role-key (ellama-tools--task-role role)))
+          (ellama-tools--start-task
+           callback
+           (ellama-tools--task-free-form-description description role-key)
+           role-key))
       (error
        (funcall callback (error-message-string err))
        nil))))
 
+(defun ellama-tools-task-from-template-tool
+    (callback template template-base arguments role)
+  "Delegate a rendered TEMPLATE to a ROLE sub-agent asynchronously.
+TEMPLATE-BASE resolves the relative TEMPLATE.  ARGUMENTS is an object whose
+keys must match the template placeholders.  CALLBACK is called once with the
+result string."
+  (condition-case err
+      (let* ((role-key (ellama-tools--task-role role))
+             (description
+              (ellama-tools--task-description
+               nil template template-base role-key arguments)))
+        (ellama-tools--start-task callback description role-key))
+    (error
+     (funcall callback (error-message-string err))
+     nil)))
+
 (ellama-tools-define-tool
  `(:function ellama-tools-task-tool
              :name "task"
              :async t
-             :description "Delegate a task to a sub-agent.
-Use either a free-form description or a template with arguments.  When using
-a template, pass arguments as an object whose keys exactly match template
-placeholders.  If validation fails, retry using the returned hint."
+             :description "Delegate a free-form task to a sub-agent.
+Pass a self-contained description and one of the listed roles.  Use
+task_from_template instead when a skill specifies a prompt template."
              :args ((:name "description" :type string
-                           :description "Free-form task prompt. Optional when 
template is provided.")
+                           :description "Self-contained task prompt for the 
sub-agent.")
                     (:name "role" :type string
-                           :enum ,(seq--into-vector (mapcar #'car 
ellama-tools-subagent-roles)))
-                    (:name "template" :type string
+                           :description "Configured sub-agent role."
+                           :enum ,(seq--into-vector
+                                   (mapcar #'car 
ellama-tools-subagent-roles))))))
+
+(ellama-tools-define-tool
+ `(:function ellama-tools-task-from-template-tool
+             :name "task_from_template"
+             :async t
+             :description "Delegate a task rendered from a prompt template.
+Use this only when instructions specify a template and its arguments.  Keys in
+arguments must exactly match the template placeholders.  If validation fails,
+retry using the returned call shape."
+             :args ((:name "template" :type string
                            :description "Template name or relative path.")
                     (:name "template_base" :type string
                            :description "Base directory for resolving a 
relative template path.")
                     (:name "arguments" :type object
-                           :description "Template substitution values keyed by 
placeholder name."))))
+                           :description "Template values keyed by placeholder 
name.")
+                    (:name "role" :type string
+                           :description "Configured sub-agent role."
+                           :enum ,(seq--into-vector
+                                   (mapcar #'car 
ellama-tools-subagent-roles))))))
 
 (provide 'ellama-tools)
 ;;; ellama-tools.el ends here
diff --git a/ellama-transient.el b/ellama-transient.el
index aa6e0bdd5be..8963b47930e 100644
--- a/ellama-transient.el
+++ b/ellama-transient.el
@@ -988,7 +988,8 @@ ARGS used for transient arguments."
 (transient-define-prefix ellama-transient-blueprint-mode-menu ()
   ["Blueprint Commands"
    ["Chat"
-    ("c" "Send to chat" ellama-send-buffer-to-new-chat-then-kill)]
+    ("c" "Send to chat" ellama-send-buffer-to-new-chat-then-kill)
+    ("a" "Plan and act" ellama-transient-plan-and-act-buffer-then-kill)]
    ["System Message"
     ("s" "Set system and chat" ellama-blueprint-chat-with-system-kill-buffer)
     ("S" "Set system and quit" ellama-blueprint-set-system-kill-buffer)]
@@ -1000,6 +1001,14 @@ ARGS used for transient arguments."
     ("k" "Kill" ellama-kill-current-buffer)
     ("q" "Quit" transient-quit-one)]])
 
+(transient-define-suffix ellama-transient-plan-and-act-buffer-then-kill ()
+  "Start a plan-and-act agent loop from current buffer and kill it."
+  (interactive)
+  (ellama-plan-and-act
+   (buffer-substring-no-properties (point-min) (point-max))
+   t)
+  (ellama-kill-current-buffer))
+
 ;;;###autoload (autoload 'ellama-transient-tools-menu "ellama-transient" nil t)
 (transient-define-prefix ellama-transient-tools-menu ()
   ["Tools Commands"
diff --git a/ellama.el b/ellama.el
index c38a17305fc..2a5855d4da2 100644
--- a/ellama.el
+++ b/ellama.el
@@ -6,7 +6,7 @@
 ;; URL: http://github.com/s-kostyaev/ellama
 ;; Keywords: help local tools
 ;; Package-Requires: ((emacs "28.1") (llm "0.31.1") (plz "0.8") (transient 
"0.7") (compat "29.1") (yaml "1.2.3"))
-;; Version: 1.29.0
+;; Version: 1.30.0
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;; Created: 8th Oct 2023
 
diff --git a/ellama.info b/ellama.info
index 5998c4f61ee..5583a121655 100644
--- a/ellama.info
+++ b/ellama.info
@@ -963,13 +963,13 @@ File: ellama.info,  Node: Task Templates Blueprints and 
Skills,  Next: Agent and
 4.1.10 Task Templates Blueprints and Skills
 -------------------------------------------
 
-   • ‘ellama-tools-task-template-dirs’: Directories where the ‘task’
-     tool searches for prompt templates when no ‘template_base’ is
-     supplied.  Relative template names are resolved below these
-     directories.
+   • ‘ellama-tools-task-template-dirs’: Directories where direct
+     ‘ellama-tools-task-from-template-tool’ calls search for prompt
+     templates when no ‘template_base’ is supplied.  Relative template
+     names are resolved below these directories.
    • ‘ellama-tools-task-template-allow-absolute-paths’: Allow absolute
-     file names in the ‘task’ tool ‘template’ argument.  Disabled by
-     default.  Relative template names are still supported.
+     file names in the ‘task_from_template’ tool ‘template’ argument.
+     Disabled by default.  Relative template names are still supported.
    • ‘ellama-blueprint-global-dir’: Global directory for storing
      blueprint files.
    • ‘ellama-blueprint-local-dir’: Local directory name for
@@ -1283,12 +1283,16 @@ File: ellama.info,  Node: Task Tool Subagents,  Next: 
Plan-and-Act Agent Loop,
 4.6 Task Tool Subagents
 =======================
 
-The ‘task’ tool delegates work to an asynchronous sub-agent.  The parent
-model gets control back immediately, and the sub-agent reports the final
-result by calling the injected ‘report_result’ tool.  If the sub-agent
-finishes a turn without reporting a result, Ellama sends
-‘ellama-tools-subagent-continue-prompt’ until the task is complete or
-‘ellama-tools-subagent-default-max-steps’ is reached.
+Ellama provides two tools for delegating work to an asynchronous
+sub-agent.  Use ‘task’ with a free-form prompt and ‘task_from_template’
+with a prompt template.  Keeping these call shapes separate makes both
+arguments and validation errors easier for local models to follow.
+
+Both tools return control to the parent model immediately.  The
+sub-agent reports its final result by calling the injected
+‘report_result’ tool.  If it finishes a turn without reporting a result,
+Ellama sends ‘ellama-tools-subagent-continue-prompt’ until the task is
+complete or ‘ellama-tools-subagent-default-max-steps’ is reached.
 
 Each sub-agent runs in its own Ellama session.  When
 ‘ellama-display-session-buffer-on-generation’ is non-nil, Ellama
@@ -1315,7 +1319,15 @@ session automatically, and continues the loop after 
compaction finishes
 or is skipped.  Any successful sub-agent response resets the consecutive
 error counter.
 
-The tool accepts either a free-form ‘description’ or a prompt template:
+The ‘task’ tool requires a non-empty ‘description’ and a configured
+‘role’:
+
+     {
+       "description": "Inspect the parser and report possible edge cases.",
+       "role": "explorer"
+     }
+
+Use ‘task_from_template’ when a skill provides a reusable prompt:
 
      {
        "template": "templates/researcher.md",
@@ -1329,15 +1341,20 @@ The tool accepts either a free-form ‘description’ or a 
prompt template:
 
 Prompt templates are plain text files with placeholders like
 ‘{main_topic}’.  The ‘arguments’ object must provide values whose keys
-match the placeholder names.  If validation fails, the tool returns an
-error with hints listing missing and unused keys and an example object
-to retry.
+match the placeholder names.  All four ‘task_from_template’ arguments
+are required.  Skills that use this tool should include the exact call
+shape and use their own directory as ‘template_base’.  If validation
+fails, the tool returns an error with the missing and unused keys and an
+example object to retry.
+
+Skills written for the older combined ‘task’ interface should change the
+tool name to ‘task_from_template’.  Their template arguments stay the
+same.
 
 Template resolution is intentionally scoped:
 
-   • relative ‘template’ paths are resolved under ‘template_base’ when
-     supplied
-   • otherwise relative paths are searched under
+   • relative ‘template’ paths are resolved under ‘template_base’
+   • direct Lisp calls can omit ‘template_base’ and search
      ‘ellama-tools-task-template-dirs’
    • path traversal outside the selected base directory is rejected
    • absolute template paths are rejected unless
@@ -2926,44 +2943,44 @@ Node: DLP and Irreversible Actions38103
 Node: Transient Menus Community Prompts and Diagnostics39218
 Node: Tool Access and Sandboxing40648
 Node: Task Templates Blueprints and Skills43720
-Node: Agent and Subagent Loops45052
-Node: Session Provider Keys46348
-Node: Session Compaction48234
-Node: Image Input50528
-Node: Audio Input52843
-Node: macOS microphone permission55511
-Node: Task Tool Subagents57672
-Node: Plan-and-Act Agent Loop60846
-Node: Edit Tool Shell Hooks63427
-Node: DLP for Tool Input/Output65720
-Node: SRT Filesystem Policy for Tools81289
-Node: Context Management86960
-Node: Transient Menus for Context Management88028
-Node: Managing the Context90102
-Node: Considerations90877
-Node: Minor modes91204
-Node: ellama-context-header-line-mode91716
-Node: ellama-context-header-line-global-mode92334
-Node: ellama-context-mode-line-mode92738
-Node: ellama-context-mode-line-global-mode93317
-Node: ellama-session-header-line-mode93711
-Node: ellama-session-mode-line-mode94215
-Node: Using Blueprints94642
-Node: Key Components95038
-Node: Creating and Managing95397
-Node: Blueprint Files96035
-Node: Variables96454
-Node: Keymap and Mode96790
-Node: Transient Menus97314
-Node: Running Blueprints Programmatically97828
-Node: MCP Integration98382
-Node: Agent Skills99560
-Node: Directory Structure99959
-Node: Creating a Skill100939
-Node: How It Works101329
-Node: Acknowledgments101735
-Node: Contributions102421
-Node: GNU Free Documentation License102946
+Node: Agent and Subagent Loops45099
+Node: Session Provider Keys46395
+Node: Session Compaction48281
+Node: Image Input50575
+Node: Audio Input52890
+Node: macOS microphone permission55558
+Node: Task Tool Subagents57719
+Node: Plan-and-Act Agent Loop61625
+Node: Edit Tool Shell Hooks64206
+Node: DLP for Tool Input/Output66499
+Node: SRT Filesystem Policy for Tools82068
+Node: Context Management87739
+Node: Transient Menus for Context Management88807
+Node: Managing the Context90881
+Node: Considerations91656
+Node: Minor modes91983
+Node: ellama-context-header-line-mode92495
+Node: ellama-context-header-line-global-mode93113
+Node: ellama-context-mode-line-mode93517
+Node: ellama-context-mode-line-global-mode94096
+Node: ellama-session-header-line-mode94490
+Node: ellama-session-mode-line-mode94994
+Node: Using Blueprints95421
+Node: Key Components95817
+Node: Creating and Managing96176
+Node: Blueprint Files96814
+Node: Variables97233
+Node: Keymap and Mode97569
+Node: Transient Menus98093
+Node: Running Blueprints Programmatically98607
+Node: MCP Integration99161
+Node: Agent Skills100339
+Node: Directory Structure100738
+Node: Creating a Skill101718
+Node: How It Works102108
+Node: Acknowledgments102514
+Node: Contributions103200
+Node: GNU Free Documentation License103725
 
 End Tag Table
 
diff --git a/tests/test-ellama-tools.el b/tests/test-ellama-tools.el
index 170e192e83b..af7bfd86f26 100644
--- a/tests/test-ellama-tools.el
+++ b/tests/test-ellama-tools.el
@@ -3172,11 +3172,16 @@ Return list with result and prompt."
                 (list "subset" :tools '("read_file" "task"))))
          (ellama-tools-available
           (list (llm-make-tool :name "task" :function #'ignore)
+                (llm-make-tool :name "task_from_template"
+                               :function #'ignore)
                 (llm-make-tool :name "read_file" :function #'ignore)
                 (llm-make-tool :name "grep" :function #'ignore))))
     (should-not
      (member "task"
              (mapcar #'llm-tool-name (ellama-tools--for-role "all"))))
+    (should-not
+     (member "task_from_template"
+             (mapcar #'llm-tool-name (ellama-tools--for-role "all"))))
     (should (equal
              (mapcar #'llm-tool-name (ellama-tools--for-role "subset"))
              '("task" "read_file")))
@@ -4115,7 +4120,7 @@ END_ELLAMA_AGENT_STATE"))
       (should (equal (plist-get (car history) :args)
                      '("file.el" "content"))))))
 
-(ert-deftest test-ellama-tools-task-tool-role-fallback-and-report-priority ()
+(ert-deftest test-ellama-tools-task-tool-report-priority ()
   (ellama-test--ensure-local-ellama-tools)
   (let ((ellama--current-session-id "parent-1")
         (ellama-tools-subagent-default-max-steps 7)
@@ -4157,7 +4162,7 @@ END_ELLAMA_AGENT_STATE"))
                    (lambda (&rest _args) nil)))
           (should (null (ellama-tools-task-tool (lambda (_res) nil)
                                                 "Do work"
-                                                "unknown-role")))
+                                                "general")))
           (should (eq resolved-provider 'provider))
           (should (equal resolved-provider-role "general"))
           (should (equal resolved-tools-role "general"))
@@ -4195,7 +4200,97 @@ END_ELLAMA_AGENT_STATE"))
       (when (buffer-live-p worker-buffer)
         (kill-buffer worker-buffer)))))
 
-(ert-deftest test-ellama-tools-task-tool-template-renders-arguments ()
+(ert-deftest test-ellama-tools-task-tool-rejects-unknown-role ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-subagent-roles
+         '(("general" :system "General." :tools nil)
+           ("explorer" :system "Explore." :tools nil)))
+        callback-msg
+        started)
+    (cl-letf (((symbol-function 'ellama-new-session)
+               (lambda (&rest _args)
+                 (setq started t))))
+      (should
+       (null
+        (ellama-tools-task-tool
+         (lambda (msg) (setq callback-msg msg))
+         "Do work"
+         "unknown-role")))
+      (should-not started)
+      (should (string-match-p "Task validation failed" callback-msg))
+      (should (string-match-p "Unknown role: unknown-role" callback-msg))
+      (should (string-match-p
+               "Available roles: general, explorer"
+               callback-msg)))))
+
+(ert-deftest test-ellama-tools-task-tool-rejects-empty-description ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-subagent-roles
+         '(("general" :system "General." :tools nil)))
+        callback-msg
+        started)
+    (cl-letf (((symbol-function 'ellama-new-session)
+               (lambda (&rest _args)
+                 (setq started t))))
+      (should
+       (null
+        (ellama-tools-task-tool
+         (lambda (msg) (setq callback-msg msg))
+         ""
+         "general")))
+      (should-not started)
+      (should (string-match-p "Task validation failed" callback-msg))
+      (should (string-match-p
+               "description must be a non-empty string"
+               callback-msg))
+      (should (string-match-p
+               "\"description\": \"\\.\\.\\.\""
+               callback-msg)))))
+
+(ert-deftest test-ellama-tools-task-tool-schemas-are-disjoint-and-required ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((task
+          (seq-find
+           (lambda (tool)
+             (string= (llm-tool-name tool) "task"))
+           ellama-tools-available))
+         (template-task
+          (seq-find
+           (lambda (tool)
+             (string= (llm-tool-name tool) "task_from_template"))
+           ellama-tools-available))
+         (task-args (llm-tool-args task))
+         (template-args (llm-tool-args template-task)))
+    (should task)
+    (should template-task)
+    (should (equal (mapcar (lambda (arg) (plist-get arg :name))
+                           task-args)
+                   '("description" "role")))
+    (should (equal (mapcar (lambda (arg) (plist-get arg :name))
+                           template-args)
+                   '("template" "template_base" "arguments" "role")))
+    (should-not (seq-some (lambda (arg) (plist-get arg :optional))
+                          task-args))
+    (should-not (seq-some (lambda (arg) (plist-get arg :optional))
+                          template-args))))
+
+(ert-deftest test-ellama-tools-task-tool-keeps-template-lisp-compatibility ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let (call)
+    (cl-letf (((symbol-function 'ellama-tools-task-from-template-tool)
+               (lambda (&rest args)
+                 (setq call args)
+                 nil)))
+      (should
+       (null
+        (ellama-tools-task-tool
+         'callback nil "explorer" "reviewer.md" "/skills" '(:topic "API"))))
+      (should
+       (equal call
+              '(callback "reviewer.md" "/skills" (:topic "API")
+                         "explorer"))))))
+
+(ert-deftest test-ellama-tools-task-from-template-renders-arguments ()
   (ellama-test--ensure-local-ellama-tools)
   (let* ((root (make-temp-file "ellama-task-template-" t))
          (templates (expand-file-name "templates" root))
@@ -4236,13 +4331,12 @@ END_ELLAMA_AGENT_STATE"))
                      (lambda (&rest _args) nil)))
             (should
              (null
-              (ellama-tools-task-tool
+              (ellama-tools-task-from-template-tool
                (lambda (msg) (setq callback-msg msg))
-               nil
-               "explorer"
                "templates/researcher.md"
                root
-               arguments)))
+               arguments
+               "explorer")))
             (should (equal new-session-prompt
                            "Goal: Map the area\nTopic: Safety\n"))
             (should (equal stream-prompt new-session-prompt))
@@ -4251,7 +4345,7 @@ END_ELLAMA_AGENT_STATE"))
         (kill-buffer worker-buffer))
       (delete-directory root t))))
 
-(ert-deftest test-ellama-tools-task-tool-template-validation-hints ()
+(ert-deftest test-ellama-tools-task-from-template-validation-hints ()
   (ellama-test--ensure-local-ellama-tools)
   (let* ((root (make-temp-file "ellama-task-template-" t))
          (arguments (make-hash-table :test #'equal))
@@ -4270,13 +4364,12 @@ END_ELLAMA_AGENT_STATE"))
                        (setq started t))))
             (should
              (null
-              (ellama-tools-task-tool
+              (ellama-tools-task-from-template-tool
                (lambda (msg) (setq callback-msg msg))
-               nil
-               "explorer"
                "researcher.md"
                root
-               arguments)))
+               arguments
+               "explorer")))
             (should-not started)
             (should (string-match-p
                      "Task template validation failed"
@@ -4286,10 +4379,13 @@ END_ELLAMA_AGENT_STATE"))
             (should (string-match-p "- topic" callback-msg))
             (should (string-match-p
                      "\"subtopic_name\": \"\\.\\.\\.\""
+                     callback-msg))
+            (should (string-match-p
+                     "Retry the task_from_template call"
                      callback-msg))))
       (delete-directory root t))))
 
-(ert-deftest test-ellama-tools-task-tool-template-rejects-traversal ()
+(ert-deftest test-ellama-tools-task-from-template-rejects-traversal ()
   (ellama-test--ensure-local-ellama-tools)
   (let* ((root (make-temp-file "ellama-task-template-" t))
          (base (expand-file-name "base" root))
@@ -4309,13 +4405,12 @@ END_ELLAMA_AGENT_STATE"))
                        (setq started t))))
             (should
              (null
-              (ellama-tools-task-tool
+              (ellama-tools-task-from-template-tool
                (lambda (msg) (setq callback-msg msg))
-               nil
-               "explorer"
                "../outside.md"
                base
-               arguments)))
+               arguments
+               "explorer")))
             (should-not started)
             (should (string-match-p
                      "Template path escapes base directory"
diff --git a/tests/test-ellama-transient.el b/tests/test-ellama-transient.el
index 514b8f4d04f..45fd004f360 100644
--- a/tests/test-ellama-transient.el
+++ b/tests/test-ellama-transient.el
@@ -549,6 +549,28 @@
     (should (equal (nreverse chat-calls)
                    '(("Ask me" t (:ephemeral t)))))))
 
+(ert-deftest test-ellama-transient-plan-and-act-buffer-then-kill ()
+  (let ((buffer (generate-new-buffer "*ellama-blueprint-agent-test*"))
+        call
+        killed-buffer)
+    (unwind-protect
+        (progn
+          (with-current-buffer buffer
+            (insert "Implement the blueprint")
+            (cl-letf (((symbol-function 'ellama-plan-and-act)
+                       (lambda (prompt create-session &rest rest)
+                         (setq call (list prompt create-session rest))))
+                      ((symbol-function 'ellama-kill-current-buffer)
+                       (lambda ()
+                         (setq killed-buffer (current-buffer))
+                         (kill-buffer (current-buffer)))))
+              (ellama-transient-plan-and-act-buffer-then-kill)))
+          (should (equal call '("Implement the blueprint" t nil)))
+          (should (eq killed-buffer buffer))
+          (should-not (buffer-live-p buffer)))
+      (when (buffer-live-p buffer)
+        (kill-buffer buffer)))))
+
 (ert-deftest test-ellama-transient-add-image-uses-ephemeral-flag ()
   (let (calls)
     (cl-letf (((symbol-function 'ellama-context-add-image)


Reply via email to