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
 

Reply via email to