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)