update docs and add agents capability

update agent plugin refs rm plugin system spec restore subagents.py

3clyp50 committed Feb 8, 2026 at 02:22 UTC a3c06827e50a3f79e4698843bded5de776933816
7 files changed +84 -139
PLUGIN_SYSTEM.md deleted
-116
@@ -1,116 +0,0 @@
1 -# Unified Plugin System - Implementation Summary
2 -
3 -This document provides a technical overview of the Plugin System implementation in Agent Zero.
4 -
5 -## Overview
6 -
7 -The Plugin System allows users and the community to create full-stack plugins that integrate seamlessly with Agent Zero. It supports backend capabilities (API endpoints, tools, helpers) and frontend UI extensions. It uses a manifest-driven approach (`plugin.json`) to declare capabilities.
8 -
9 -## Architecture
10 -
11 -### Components
12 -
13 -1. Backend Plugin Discovery (`python/helpers/plugins.py`)
14 - - Resolves plugin directories across repo (`plugins/`) and user (`usr/plugins/`) locations.
15 - - Loads and validates `plugin.json` manifests.
16 - - Provides `import_plugin_module()` for dynamic dependency injection, replacing static imports.
17 - - Implements override logic (user plugins override repo plugins).
18 -
19 -2. API Endpoints
20 - - `POST /plugins_resolve` - Resolves a plugin manifest by ID (for frontend).
21 - - `GET/POST /plugins_list` - Lists all available plugins.
22 - - `GET /plugins/<id>/<path>` - Serves plugin static assets (UI, scripts).
23 -
24 -3. Frontend Plugin Loader (`webui/js/plugins.js`)
25 - - Discovers and loads `<x-extension>` tags.
26 - - Fetches plugin manifests via `/plugins_resolve`.
27 - - Path Traversal Strategy: Integrates with the standard component loader by adjusting paths (e.g., prepending `components/../` to plugin URLs) to bypass default path restrictions without modifying the core loader.
28 - - Handles module imports and props merging.
29 -
30 -4. Standard Component Loader (`webui/js/components.js`)
31 - - Vanilla Implementation: Remains unmodified to ensure stability and compatibility.
32 - - Enforces `components/` prefix for all loaded resources.
33 - - Used by `plugins.js` via relative path traversal to load plugin UI components.
34 -
35 -5. DOM Integration
36 - - Automatic loading on DOM ready.
37 - - MutationObserver for dynamic plugin injection.
38 - - Alpine.js integration via `globalThis.xAttrs()`.
39 -
40 -## File Structure
41 -
42 -```
43 -/plugins/ # Repo plugins (default)
44 - ├── memory/
45 - │ ├── plugin.json # Manifest
46 - │ ├── api/ # Backend endpoints
47 - │ ├── tools/ # Agent tools
48 - │ ├── helpers/ # Python helpers
49 - │ ├── ui/ # Frontend components
50 - │ │ ├── memory-dashboard.html
51 - │ │ └── memory-dashboard-store.js
52 - │ └── prompts/ # System prompts
53 - └── README.md
54 -
55 -/usr/plugins/ # User plugins (override)
56 - ├── my-plugin/
57 - │ ├── plugin.json
58 - │ └── ...
59 - └── README.md
60 -```
61 -
62 -## Plugin Manifest Schema
63 -
64 -```json
65 -{
66 - "id": "plugin-id", // Required: Must match directory name
67 - "name": "Display Name", // Optional: Human-readable name
68 - "provides": {
69 - "api": [
70 - { "module": "api/my_endpoint.py", "description": "..." }
71 - ],
72 - "tool": [
73 - { "module": "tools/my_tool.py", "description": "..." }
74 - ],
75 - "ui": {
76 - "component": "ui/index.html",
77 - "module": "ui/main.js"
78 - },
79 - "prompt": [
80 - { "module": "prompts/system.md", "description": "..." }
81 - ]
82 - },
83 - "props": { // Optional: Default UI properties
84 - "key": "value"
85 - }
86 -}
87 -```
88 -
89 -## Usage
90 -
91 -### Creating a Plugin
92 -
93 -1. Create plugin directory: `plugins/my-plugin`
94 -2. Create `plugin.json` declaring provided capabilities.
95 -3. Implement backend modules in `api/`, `tools/`, etc.
96 -4. Implement frontend components in `ui/`.
97 -
98 -### Using Plugin Capabilities
99 -
100 -Backend (Python):
101 -Instead of static imports, use the dynamic loader:
102 -```python
103 -from python.helpers.plugins import import_plugin_module
104 -
105 -# Dynamically load a helper from the 'memory' plugin
106 -memory = import_plugin_module("memory", "helpers/memory.py")
107 -memory.some_function()
108 -```
109 -
110 -Frontend (HTML/JS):
111 -Plugins can be embedded via `<x-extension>` or loaded dynamically via `openModal`.
112 -```javascript
113 -// Open a plugin component in a modal
114 -// Uses relative path to traverse out of 'components/' and into 'plugins/'
115 -openModal("../plugins/my-plugin/ui/dashboard.html");
116 -```
plugins/example_agent/agents/plugin_example/agent.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "title": "Plugin Example Agent",
3 + "description": "This is an example agent profile loaded from the 'example_agent' plugin.",
4 + "context": "You are an example agent provided by the 'example_agent' plugin. Your purpose is to demonstrate how plugins can distribute agent profiles."
5 +}
plugins/example_agent/agents/plugin_example/prompts/agent.system.plugin_example.md new
+3
@@ -0,0 +1,3 @@
1 +You are the Example Plugin Agent.
2 +You were loaded from the `plugins/example_agent` directory.
3 +Your main goal is to demonstrate that agent profiles can be distributed via plugins.
plugins/example_agent/plugin.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "id": "example_agent",
3 + "name": "Example Agent Plugin",
4 + "version": "0.1.0",
5 + "description": "An example plugin demonstrating the agent profile capability.",
6 + "author": "Agent Zero",
7 + "provides": {
8 + "agent": [
9 + {
10 + "dir": "agents",
11 + "description": "Example agent profile"
12 + }
13 + ]
14 + }
15 +}
python/helpers/plugins.py
+22 -12
@@ -144,9 +144,10 @@ def _extract_module_dir(module_path: str) -> str:
144 def get_plugin_paths(cap_type: str, *subpaths: str) -> List[str]:
145 """
146 Get all paths from loaded plugins for a given capability type.
147 + Supports both 'module' (parent dir used) and 'dir' (direct path used) in config.
148
149 Args:
149 - cap_type: Capability type (e.g., "tool", "extension", "api")
150 + cap_type: Capability type (e.g., "tool", "extension", "api", "agent")
151 subpaths: Additional path components to append
152
153 Returns:
@@ -160,18 +161,27 @@ def get_plugin_paths(cap_type: str, *subpaths: str) -> List[str]:
161 continue
162
163 # Normalize to list for uniform processing
163 - modules = []
164 + items = []
165 if isinstance(cap_config, list):
165 - modules = [cap.get("module") for cap in cap_config if isinstance(cap, dict) and cap.get("module")]
166 - elif isinstance(cap_config, dict) and cap_config.get("module"):
167 - modules = [cap_config["module"]]
166 + items = [c for c in cap_config if isinstance(c, dict)]
167 + elif isinstance(cap_config, dict):
168 + items = [cap_config]
169
169 - # Build paths from modules
170 - for module_path in modules:
171 - module_dir = _extract_module_dir(module_path)
172 - full_path = files.get_abs_path(str(plugin.path), module_dir, *subpaths)
173 - if files.exists(full_path) and full_path not in paths:
174 - paths.append(full_path)
170 + # Build paths from items
171 + for item in items:
172 + path_to_add = ""
173 +
174 + # Priority 1: Explicit directory
175 + if item.get("dir"):
176 + path_to_add = item["dir"]
177 + # Priority 2: Module parent directory
178 + elif item.get("module"):
179 + path_to_add = _extract_module_dir(item["module"])
180 +
181 + if path_to_add:
182 + full_path = files.get_abs_path(str(plugin.path), path_to_add, *subpaths)
183 + if files.exists(full_path) and full_path not in paths:
184 + paths.append(full_path)
185
186 return paths
187
@@ -259,4 +269,4 @@ def build_plugin_response_data(plugin: Plugin) -> dict:
269 if ui_config.get("props"):
270 response_data["props"] = ui_config["props"]
271
262 - return response_data
\ No newline at end of file
272 + return response_data
python/helpers/settings.py
+4 -5
@@ -7,7 +7,7 @@ import subprocess
7 from typing import Any, Literal, TypedDict, cast, TypeVar
8
9 import models
10 -from python.helpers import runtime, whisper, defer, git
10 +from python.helpers import runtime, whisper, defer, git, subagents
11 from . import files, dotenv
12 from python.helpers.print_style import PrintStyle
13 from python.helpers.providers import get_providers, FieldOption as ProvidersFO
@@ -248,9 +248,9 @@ def convert_out(settings: Settings) -> SettingsOutput:
248 embedding_providers=get_providers("embedding"),
249 shell_interfaces=[{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}],
250 is_dockerized=runtime.is_dockerized(),
251 - agent_subdirs=[{"value": subdir, "label": subdir}
252 - for subdir in files.get_subdirectories("agents")
253 - if subdir != "_example"],
251 + agent_subdirs=[{"value": item["key"], "label": item["label"]}
252 + for item in subagents.get_all_agents_list()
253 + if item["key"] != "_example"],
254 knowledge_subdirs=[{"value": subdir, "label": subdir}
255 for subdir in files.get_subdirectories("knowledge", exclude="default")],
256 stt_models=[
@@ -815,4 +815,3 @@ def create_auth_token() -> str:
815
816 def _get_version():
817 return git.get_version()
818 -
python/helpers/subagents.py
+35 -6
@@ -10,7 +10,7 @@ USER_DIR = "usr"
10 DEFAULT_AGENTS_DIR = "agents"
11 USER_AGENTS_DIR = "usr/agents"
12
13 -type Origin = Literal["default", "user", "project"]
13 +type Origin = Literal["default", "user", "project", "plugin"]
14
15 if TYPE_CHECKING:
16 from agent import Agent
@@ -57,10 +57,18 @@ def get_agents_dict(
57 )
58 return merged
59
60 - # load default and custom agents and merge
60 + # load default, plugin, and custom agents and merge
61 default_agents = _get_agents_list_from_dir(DEFAULT_AGENTS_DIR, origin="default")
62 + merged: dict[str, SubAgentListItem] = dict(default_agents)
63 +
64 + # merge with plugin agents
65 + from python.helpers import plugins
66 + for plugin_dir in reversed(plugins.get_plugin_paths("agent")):
67 + plugin_agents = _get_agents_list_from_dir(plugin_dir, origin="plugin")
68 + merged = _merge_agent_dicts(merged, plugin_agents)
69 +
70 custom_agents = _get_agents_list_from_dir(USER_AGENTS_DIR, origin="user")
63 - merged = _merge_agent_dicts(default_agents, custom_agents)
71 + merged = _merge_agent_dicts(merged, custom_agents)
72
73 # merge with project agents if possible
74 if project_name:
@@ -102,12 +110,20 @@ def load_agent_data(name: str, project_name: str | None = None) -> SubAgent:
110 return original
111 return override
112
105 - # load default and user agents and merge
113 + # load default, plugin, and user agents and merge
114 default_agent = _load_agent_data_from_dir(
115 DEFAULT_AGENTS_DIR, name, origin="default"
116 )
117 + merged = default_agent
118 +
119 + # merge with plugin agents
120 + from python.helpers import plugins
121 + for plugin_dir in reversed(plugins.get_plugin_paths("agent")):
122 + plugin_agent = _load_agent_data_from_dir(plugin_dir, name, origin="plugin")
123 + merged = _merge_agent(merged, plugin_agent)
124 +
125 user_agent = _load_agent_data_from_dir(USER_AGENTS_DIR, name, origin="user")
110 - merged = _merge_agent(default_agent, user_agent)
126 + merged = _merge_agent(merged, user_agent)
127
128 # merge with project agent if possible
129 if project_name:
@@ -121,7 +137,7 @@ def load_agent_data(name: str, project_name: str | None = None) -> SubAgent:
137
138 if merged is None:
139 raise FileNotFoundError(
124 - f"Agent '{name}' not found in default or custom directories"
140 + f"Agent '{name}' not found in default, plugin, or custom directories"
141 )
142
143 return merged
@@ -222,9 +238,13 @@ def _merge_agent_list_items(
238
239
240 def get_agents_roots() -> list[str]:
241 + from python.helpers import plugins
242 +
243 + plugin_agents = list(reversed(plugins.get_plugin_paths("agent")))
244 project_agents = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/agents")
245 paths = [
246 files.get_abs_path(DEFAULT_AGENTS_DIR),
247 + *plugin_agents,
248 files.get_abs_path(USER_AGENTS_DIR),
249 *project_agents,
250 ]
@@ -249,6 +269,8 @@ def get_all_agents_list() -> list[dict[str, str]]:
269 return "project"
270 if rel.startswith("usr/agents"):
271 return "user"
272 + if "/plugins/" in rel or rel.startswith("plugins/"):
273 + return "plugin"
274 return "default"
275
276 merged: dict[str, SubAgentListItem] = {}
@@ -339,6 +361,13 @@ def get_paths(
361 if (not must_exist_completely) or files.exists(files.get_abs_path(USER_AGENTS_DIR, profile_name, *check_subpaths)):
362 paths.append(path)
363
364 + # plugin agents/<profile>/...
365 + from python.helpers import plugins
366 + for plugin_dir in plugins.get_plugin_paths("agent"):
367 + path = files.get_abs_path(plugin_dir, profile_name, *subpaths)
368 + if (not must_exist_completely) or files.exists(files.get_abs_path(plugin_dir, profile_name, *check_subpaths)):
369 + paths.append(path)
370 +
371 # agents/<profile>/...
372 path = files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *subpaths)
373 if (not must_exist_completely) or files.exists(files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *check_subpaths)):