branch: externals/ellama
commit 366f05f7abbb80eb7d3c08055e2c3d26bd0654ac
Merge: d50e672bf9 36be8cc6f0
Author: Sergey Kostyaev <[email protected]>
Commit: GitHub <[email protected]>
Merge pull request #374 from s-kostyaev/add-subagents
Add subagents support
---
NEWS.org | 12 +++
README.org | 4 +
ellama-tools.el | 263 ++++++++++++++++++++++++++++++++++++++++----------------
ellama.el | 12 +--
ellama.info | 67 ++++++++-------
5 files changed, 240 insertions(+), 118 deletions(-)
diff --git a/NEWS.org b/NEWS.org
index 7d7b87cb24..b4bf00f841 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -1,3 +1,15 @@
+* Version 1.12.0
+- Add asynchronous task delegation functionality via the new ~task~ tool that
+ allows delegating descriptions to subagents with custom roles and max-steps
+ limits.
+- Add configurable subagent roles with ~ellama-tools-subagent-roles~
+ customization variable, enabling custom providers, system prompts and tool
+ access permissions for different agent types.
+- Add JSON support to answer variants in the ~ellama-tools-ask-user-tool~ to
+ handle both list and string inputs.
+- Simplify tool result handling by adding nil checks before JSON encoding.
+- Remove redundant tool definitions including ~enable_tool~, ~disable_tool~,
+ ~search_tools~, and ~today~ to streamline the tool management system.
* Version 1.11.1
- Add AGENTS.md support to system message. Include the AGENTS.md file content
from the current project or subproject in the system message by adding helper
diff --git a/README.org b/README.org
index e3f7ceb6d2..033483505a 100644
--- a/README.org
+++ b/README.org
@@ -388,6 +388,10 @@ argument generated text string.
Skills.
- ~ellama-skills-local-path~: Project-relative path for local Agent Skills.
Default value is ~"skills"~.
+- ~ellama-tools-subagent-default-max-steps~: Default maximum number of
+ auto-continue steps for a sub-agent. Default value is 30.
+- ~ellama-tools-subagent-roles~: Subagent roles with provider, system prompt
and
+ allowed tools. Configuration of subagents for the ~task~ tool.
* Context Management
diff --git a/ellama-tools.el b/ellama-tools.el
index a2b6b63e59..f69a209847 100644
--- a/ellama-tools.el
+++ b/ellama-tools.el
@@ -50,6 +50,66 @@ Tools from this list will work without user confirmation."
:type 'integer
:group 'ellama)
+(defcustom ellama-tools-subagent-default-max-steps 30
+ "Default maximum number of auto-continue steps for a sub-agent."
+ :type 'integer
+ :group 'ellama)
+
+(defcustom ellama-tools-subagent-continue-prompt "Task not marked complete.
Continue working. If you are done, YOU MUST use the `report_result` tool."
+ "Prompt sent to sub-agent to keep the loop going."
+ :type 'string
+ :group 'ellama)
+
+(defcustom ellama-tools-subagent-roles
+ '(("general"
+ :system "You are a helpful general assistant."
+ :tools :all)
+
+ ("explorer"
+ :system "Explore, inspect, and report findings. Do not modify files."
+ :tools ("read_file" "directory_tree" "grep" "grep_in_file"
+ "count_lines" "lines_range" "project_root" "shell_command"))
+
+ ("coder"
+ :system "You are an expert software developer. Make precise changes."
+ :tools ("read_file" "write_file" "edit_file" "append_file" "prepend_file"
+ "move_file" "apply_patch" "grep" "grep_in_file" "project_root"
+ "directory_tree" "count_lines" "lines_range" "shell_command")
+ :provider 'ellama-coding-provider)
+
+ ("bash"
+ :system "You are a bash scripting expert."
+ :tools ("shell_command")
+ :provider 'ellama-coding-provider))
+
+ "Subagent roles with provider, system prompt and allowed tools."
+ :type '(alist :key-type string :value-type plist)
+ :group 'ellama)
+
+(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)
+ ellama-tools-available)
+ ((listp tools)
+ (cl-remove-if-not
+ (lambda (tool) (member (llm-tool-name tool) tools))
+ ellama-tools-available))
+ (t
+ nil))))
+
+(defun ellama-tools--provider-for-role (role)
+ "Resolve provider for ROLE."
+ (let* ((cfg (cdr (assoc role ellama-tools-subagent-roles)))
+ (provider (plist-get cfg :provider)))
+ (if (not provider)
+ ellama-provider
+ (while (not (llm-standard-provider-p provider))
+ (setq provider (eval provider)))
+ provider)))
+
(defvar ellama-tools-available nil
"Alist containing all registered tools.")
@@ -108,9 +168,9 @@ FUNCTION if approved, \"Forbidden by the user\" otherwise."
;; No - return nil
((eq answer ?n)
"Forbidden by the user"))))
- (if (stringp result)
- result
- (json-encode result)))))))
+ (when result (if (stringp result)
+ result
+ (json-encode result))))))))
(defun ellama-tools-wrap-with-confirm (tool-plist)
"Wrap a tool's function with automatic confirmation.
@@ -165,21 +225,6 @@ TOOL-PLIST is a property list in the format expected by
`llm-make-tool'."
(mapcar (lambda (tool) (llm-tool-name tool))
ellama-tools-available))))))
(ellama-tools-enable-by-name-tool tool-name)))
-(ellama-tools-define-tool
- '(:function
- ellama-tools-enable-by-name-tool
- :name
- "enable_tool"
- :args
- ((:name
- "name"
- :type
- string
- :description
- "Name of the tool to enable."))
- :description
- "Enable each tool that matches NAME. You need to reply to the user before
using newly enabled tool."))
-
(defun ellama-tools-disable-by-name-tool (name)
"Remove from `ellama-tools-enabled' each tool that matches NAME."
(let* ((tool (seq-find (lambda (tool) (string= name (llm-tool-name tool)))
@@ -197,21 +242,6 @@ TOOL-PLIST is a property list in the format expected by
`llm-make-tool'."
(mapcar (lambda (tool) (llm-tool-name tool))
ellama-tools-enabled)))))
(ellama-tools-disable-by-name-tool tool-name)))
-(ellama-tools-define-tool
- '(:function
- ellama-tools-disable-by-name-tool
- :name
- "disable_tool"
- :args
- ((:name
- "name"
- :type
- string
- :description
- "Name of the tool to disable."))
- :description
- "Disable each tool that matches NAME."))
-
;;;###autoload
(defun ellama-tools-enable-all ()
"Enable all available tools."
@@ -522,48 +552,6 @@ Replace OLDCONTENT with NEWCONTENT."
:description
"List all available tools."))
-(defun ellama-tools-search-tool (search-string)
- "Search available tools that matches SEARCH-STRING."
- (json-encode
- (cl-remove-if-not
- (lambda (item)
- (or (string-match-p search-string (alist-get "name" item nil nil
'string=))
- (string-match-p search-string (alist-get "description" item nil nil
'string=))))
- (mapcar
- (lambda (tool)
- `(("name" . ,(llm-tool-name tool))
- ("description" . ,(llm-tool-description tool))))
- ellama-tools-available))))
-
-(ellama-tools-define-tool
- '(:function
- ellama-tools-search-tool
- :name
- "search_tools"
- :args
- ((:name
- "search-string"
- :type
- string
- :description
- "String to search for in tool names or descriptions."))
- :description
- "Search available tools that matches SEARCH-STRING."))
-
-(defun ellama-tools-today-tool ()
- "Return current date."
- (format-time-string "%Y-%m-%d"))
-
-(ellama-tools-define-tool
- '(:function
- ellama-tools-today-tool
- :name
- "today"
- :args
- nil
- :description
- "Return current date."))
-
(defun ellama-tools-now-tool ()
"Return current date, time and timezone."
(format-time-string "%Y-%m-%d %H:%M:%S %Z"))
@@ -596,7 +584,10 @@ Replace OLDCONTENT with NEWCONTENT."
(defun ellama-tools-ask-user-tool (question answer-variant-list)
"Ask user a QUESTION to receive a clarification.
ANSWER-VARIANT-LIST is a list of possible answer variants."
- (completing-read (concat question " ") (seq--into-list answer-variant-list)))
+ (completing-read (concat question " ")
+ (if (stringp answer-variant-list)
+ (seq--into-list (json-parse-string answer-variant-list))
+ (seq--into-list answer-variant-list))))
(ellama-tools-define-tool
'(:function
@@ -731,5 +722,125 @@ Returns the output of the patch command or an error
message."
:description
"Apply a patch to the file at PATH."))
+(defun ellama-tools--make-report-result-tool (callback session)
+ "Make report_result tool dynamically for SESSION.
+CALLBACK will be used to report result asyncronously."
+ `(:function
+ (lambda (result)
+ (let* ((extra (ellama-session-extra ,session))
+ (done (plist-get extra :task-completed)))
+ (unless done
+ (setf (ellama-session-extra ,session)
+ (plist-put extra :task-completed t))
+ (funcall ,callback result)))
+ "Result received. Task completed.")
+ :name "report_result"
+ :description "Report final result and terminate the task."
+ :args ((:name "result" :type string))))
+
+(defun ellama--subagent-loop-handler (_text)
+ "Internal subagent loop handler."
+ (let* ((session ellama--current-session)
+ (extra (ellama-session-extra session))
+ (done (plist-get extra :task-completed))
+ (steps (or (plist-get extra :step-count) 0))
+ (max (or (plist-get extra :max-steps)
+ ellama-tools-subagent-default-max-steps))
+ (callback (plist-get extra :result-callback)))
+ (cond
+ (done
+ (message "Subagent finished."))
+ ((>= steps max)
+ (setf (ellama-session-extra session)
+ (plist-put extra :task-completed t))
+ (funcall callback (format "Max steps (%d) reached." max)))
+ (t
+ (setf (ellama-session-extra session)
+ (plist-put extra :step-count (1+ steps)))
+ (ellama-stream
+ ellama-tools-subagent-continue-prompt
+ :session session
+ :on-done #'ellama--subagent-loop-handler)))))
+
+(defun ellama-tools-task-tool (callback description &optional role)
+ "Delegate DESCRIPTION to a sub-agent asynchronously.
+
+CALLBACK – function called once with the result string.
+ROLE – role key from `ellama-tools-subagent-roles'."
+ (let* ((parent-id ellama--current-session-id)
+
+ ;; ---- role resolution (safe fallback) ----
+ (role-key (if (assoc role ellama-tools-subagent-roles)
+ role
+ "general"))
+
+ (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))
+
+ (steps-limit ellama-tools-subagent-default-max-steps)
+
+ ;; ---- create ephemeral worker session ----
+ (worker (ellama-new-session provider description t))
+
+ ;; ---- resolve tools for role ----
+ (role-tools (ellama-tools--for-role role-key))
+
+ ;; ---- 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 role-tools)))
+
+ ;; ============================================================
+ ;; Initialize session state (single source of truth)
+ ;; ============================================================
+
+ (setf (ellama-session-extra worker)
+ (list
+ :parent-session parent-id
+ :role role-key
+ :tools all-tools
+ :result-callback callback
+ :task-completed nil
+ :step-count 0
+ :max-steps steps-limit))
+
+ ;; ============================================================
+ ;; Start the agent loop
+ ;; ============================================================
+
+ (ellama-stream
+ description
+ :session worker
+ :on-done #'ellama--subagent-loop-handler
+ :tools all-tools
+ :system
+ (format
+ "%s\n\nINSTRUCTIONS:\n\
+Work step-by-step. Use tools when needed.\n\
+When the task is COMPLETE you MUST call `report_result` exactly once."
+ system-msg))
+
+ ;; ============================================================
+ ;; 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))
+
+(ellama-tools-define-tool
+ `(:function ellama-tools-task-tool
+ :name "task"
+ :async t
+ :description "Delegate a task to a sub-agent."
+ :args ((:name "description" :type string)
+ (:name "role" :type string
+ :enum ,(seq--into-vector (mapcar #'car
ellama-tools-subagent-roles))))))
+
(provide 'ellama-tools)
;;; ellama-tools.el ends here
diff --git a/ellama.el b/ellama.el
index 3733206d40..a350cfc85a 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.24.0") (plz "0.8") (transient
"0.7") (compat "29.1") (yaml "1.2.3"))
-;; Version: 1.11.1
+;; Version: 1.12.0
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Created: 8th Oct 2023
@@ -468,16 +468,6 @@ It should be a function with single argument generated
text string."
"Enable debug."
:type 'boolean)
-(defcustom ellama-subagent-default-max-steps 30
- "Default maximum number of auto-continue steps for a sub-agent."
- :type 'integer
- :group 'ellama)
-
-(defcustom ellama-subagent-continue-prompt "Task not marked complete. Continue
working. If you are done, YOU MUST use the `report_result` tool."
- "Prompt sent to sub-agent to keep the loop going."
- :type 'string
- :group 'ellama)
-
(defun ellama--set-file-name-and-save ()
"Set buffer file name and save buffer."
(interactive)
diff --git a/ellama.info b/ellama.info
index 17a2e7a7a0..c250274d11 100644
--- a/ellama.info
+++ b/ellama.info
@@ -521,6 +521,11 @@ argument generated text string.
containing Agent Skills.
• ‘ellama-skills-local-path’: Project-relative path for local Agent
Skills. Default value is ‘"skills"’.
+ • ‘ellama-tools-subagent-default-max-steps’: Default maximum number
+ of auto-continue steps for a sub-agent. Default value is 30.
+ • ‘ellama-tools-subagent-roles’: Subagent roles with provider, system
+ prompt and allowed tools. Configuration of subagents for the
+ ‘task’ tool.
File: ellama.info, Node: Context Management, Next: Minor modes, Prev:
Configuration, Up: Top
@@ -1574,37 +1579,37 @@ Node: Installation3748
Node: Commands8756
Node: Keymap16195
Node: Configuration19028
-Node: Context Management25551
-Node: Transient Menus for Context Management26459
-Node: Managing the Context28073
-Node: Considerations28848
-Node: Minor modes29441
-Node: ellama-context-header-line-mode31429
-Node: ellama-context-header-line-global-mode32254
-Node: ellama-context-mode-line-mode32974
-Node: ellama-context-mode-line-global-mode33822
-Node: Ellama Session Header Line Mode34526
-Node: Enabling and Disabling35095
-Node: Customization35542
-Node: Ellama Session Mode Line Mode35830
-Node: Enabling and Disabling (1)36415
-Node: Customization (1)36862
-Node: Using Blueprints37156
-Node: Key Components of Ellama Blueprints37796
-Node: Creating and Managing Blueprints38403
-Node: Blueprints files39381
-Node: Variable Management39802
-Node: Keymap and Mode40255
-Node: Transient Menus41191
-Node: Running Blueprints programmatically41737
-Node: MCP Integration42324
-Node: Agent Skills43346
-Node: Directory Structure43709
-Node: Creating a Skill44736
-Node: How it works45111
-Node: Acknowledgments45502
-Node: Contributions46213
-Node: GNU Free Documentation License46599
+Node: Context Management25863
+Node: Transient Menus for Context Management26771
+Node: Managing the Context28385
+Node: Considerations29160
+Node: Minor modes29753
+Node: ellama-context-header-line-mode31741
+Node: ellama-context-header-line-global-mode32566
+Node: ellama-context-mode-line-mode33286
+Node: ellama-context-mode-line-global-mode34134
+Node: Ellama Session Header Line Mode34838
+Node: Enabling and Disabling35407
+Node: Customization35854
+Node: Ellama Session Mode Line Mode36142
+Node: Enabling and Disabling (1)36727
+Node: Customization (1)37174
+Node: Using Blueprints37468
+Node: Key Components of Ellama Blueprints38108
+Node: Creating and Managing Blueprints38715
+Node: Blueprints files39693
+Node: Variable Management40114
+Node: Keymap and Mode40567
+Node: Transient Menus41503
+Node: Running Blueprints programmatically42049
+Node: MCP Integration42636
+Node: Agent Skills43658
+Node: Directory Structure44021
+Node: Creating a Skill45048
+Node: How it works45423
+Node: Acknowledgments45814
+Node: Contributions46525
+Node: GNU Free Documentation License46911
End Tag Table