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)):