simplification and further memory decoupling
Alessandro committed
Feb 15, 2026 at 02:51 UTC
e18efbb115a3bdfb293d39d3e4d8b4c667acb01a
40 files changed
+281
-931
agent.py
+4
-15
@@ -19,7 +19,6 @@ from python.helpers import (
19
context as context_helper,
20
dirty_json,
21
subagents,
22
- plugins
22
)
23
from python.helpers.print_style import PrintStyle
24
@@ -645,21 +644,15 @@ class Agent:
644
return system_prompt
645
646
def parse_prompt(self, _prompt_file: str, **kwargs):
648
- dirs = subagents.get_paths(self, "prompts")
647
+ dirs = subagents.get_paths(self, "prompts", include_plugins=True)
648
650
- # Plugin prompt paths
651
- dirs.extend(plugins.get_plugin_paths("prompt"))
652
-
649
prompt = files.parse_file(
650
_prompt_file, _directories=dirs, _agent=self, **kwargs
651
)
652
return prompt
653
654
def read_prompt(self, file: str, **kwargs) -> str:
659
- dirs = subagents.get_paths(self, "prompts")
660
-
661
- # Plugin prompt paths
662
- dirs.extend(plugins.get_plugin_paths("prompt"))
655
+ dirs = subagents.get_paths(self, "prompts", include_plugins=True)
656
657
prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs)
658
if files.is_full_json_template(prompt):
@@ -995,12 +988,8 @@ class Agent:
988
classes = []
989
990
# search for tools in agent's folder hierarchy
998
- paths = subagents.get_paths(self, "tools", name + ".py", default_root="python")
999
-
1000
- # Add plugin tool paths
1001
- plugin_paths = plugins.get_plugin_paths("tool", name + ".py")
1002
- paths.extend(plugin_paths)
1003
-
991
+ paths = subagents.get_paths(self, "tools", name + ".py", default_root="python", include_plugins=True)
992
+
993
for path in paths:
994
try:
995
classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type]
plugins/README.md
+37
-67
@@ -2,83 +2,53 @@
2
3
This directory contains default plugins shipped with Agent Zero.
4
5
-## Plugin Architecture
6
-
7
-Agent Zero uses a unified capability-based plugin system where a single `plugin.json` manifest declares all capabilities via a `provides` dictionary. Each capability type maps to an integration point in the framework.
8
-
9
-## Plugin Structure
10
-
11
-Each plugin directory should contain:
12
-- `plugin.json` - Manifest file with capability declarations
13
-- Capability-specific files organized by type (helpers, tools, extensions, api, ui, etc.)
14
-- Other assets (CSS, images, documentation)
15
-
16
-## Manifest Schema
17
-
18
-The `plugin.json` uses a `provides` dict where keys are integration point types:
19
-
20
-```json
21
-{
22
- "id": "my-plugin",
23
- "name": "My Plugin",
24
- "version": "1.0.0",
25
- "author": "Author Name",
26
- "description": "Plugin description",
27
- "tags": ["tag1", "tag2"],
28
- "provides": {
29
- "helper": [
30
- { "module": "helpers/my_helper.py", "description": "Helper module description" }
31
- ],
32
- "tool": [
33
- { "module": "tools/my_tool.py", "description": "Tool description" }
34
- ],
35
- "extension": [
36
- { "module": "extensions/hook_name/my_extension.py", "description": "Extension description" }
37
- ],
38
- "api": [
39
- { "module": "api/my_api.py", "description": "API endpoint description" }
40
- ],
41
- "ui": {
42
- "component": "ui/component.html",
43
- "module": "ui/main.js",
44
- "description": "UI component description"
45
- }
46
- }
47
-}
48
-```
5
+## Architecture
6
+
7
+Agent Zero uses a convention-over-configuration plugin model:
8
+
9
+- `plugin.json` is not used by runtime discovery.
10
+- Runtime capabilities are discovered from directory structure.
11
+- Backend owns discovery and routing; frontend consumes resolved URLs.
12
+
13
+## Directory Conventions
14
50
-## Capability Types
15
+Each plugin lives in `plugins/<plugin_id>/` (or `usr/plugins/<plugin_id>/` for overrides).
16
52
-- **helper**: Python modules providing reusable functionality (imported via proxy or direct)
53
-- **tool**: Agent tools extending agent capabilities
54
-- **extension**: Extension hooks that run at specific lifecycle points
55
-- **api**: Flask API endpoints (ApiHandler subclasses)
56
-- **ui**: Frontend components loaded via `<x-extension>` tags
57
-- **prompt**: Custom prompt templates
58
-- **knowledge**: Knowledge base files and directories
59
-- **instrument**: Custom instrumentation and monitoring
17
+Capability discovery is based on these paths:
18
61
-## Using Plugins
19
+- `api/*.py` - API handlers (`ApiHandler` subclasses), exposed as `/plugins/{plugin_id}/{handler_name}`
20
+- `tools/*.py` - Agent tools (`Tool` subclasses)
21
+- `helpers/*.py` - Shared Python helpers
22
+- `extensions/backend/{extension_point}/*.py` - Backend lifecycle extensions
23
+- `extensions/frontend/**/*.html` - Frontend UI components (auto-injected via `<meta name="plugin-target">`)
24
+- `prompts/**/*.md` - Prompt templates
25
+- `agents/` - Agent profiles
26
+- `extensions/frontend/` - Frontend UI assets and auto-injected components
27
+
28
+## Frontend Auto-Injection (PoC)
29
+
30
+Plugins can inject HTML components into the core UI. Place components under `extensions/frontend/` and declare the injection target with a `<meta>` tag:
31
63
-### UI Components
32
```html
65
-<x-extension id="plugin-id"></x-extension>
33
+<meta name="plugin-target" content=".quick-actions-dropdown">
34
```
35
68
-### Backend Capabilities
69
-Backend capabilities (helpers, tools, extensions, APIs) are automatically discovered and integrated when the plugin is loaded.
70
-
71
-## User Plugins
36
+Components without the meta tag (e.g. modals, dashboards) are standalone and not auto-injected.
37
73
-User-created plugins should be placed in one of the following directories:
38
+Resolution flow:
39
75
-- `/usr/plugins/` (Global user plugins)
76
-- `/usr/projects/<project_name>/.a0proj/plugins/` (Project-specific plugins)
40
+1. The backend parses `<meta name="plugin-target">` from each component HTML at scan time.
41
+2. `/plugins_resolve` returns component URLs with their target selectors.
42
+3. `plugins.js` (loaded globally) creates `<x-component>` elements at the declared target selectors.
43
+4. The standard `components.js` MutationObserver handles loading automatically.
44
+5. A MutationObserver in `plugins.js` retries for targets that appear after initial page render.
45
78
-Note: `/usr/` refers to the Agent Zero user data directory in the application root, not the system `/usr` directory.
46
+## Routes
47
80
-User plugins with the same ID as repo plugins will completely override the repo version.
48
+- Plugin static assets: `GET /plugins/<plugin_id>/<path>`
49
+- Plugin APIs: `/plugins/<plugin_id>/<handler>`
50
82
-## Documentation
51
+## Notes
52
84
-See [docs/extensibility.md](../docs/extensibility.md) for complete documentation on creating plugins.
53
+- User plugins in `usr/plugins/` override repo plugins by plugin ID.
54
+- Runtime behavior is fully convention-driven from directory structure.
plugins/example_agent/plugin.json
deleted
-15
@@ -1,15 +0,0 @@
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
-}
plugins/memory/api/import_knowledge.py
renamed
+4
-5
@@ -1,9 +1,8 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
from python.helpers import files
3
-from python.helpers.plugins import import_plugin_module
4
-memory = import_plugin_module("memory", "helpers/memory.py")
5
-import os
3
from python.helpers.security import safe_filename
4
+from plugins.memory.helpers.memory import Memory, get_custom_knowledge_subdir_abs
5
+import os
6
7
8
class ImportKnowledge(ApiHandler):
@@ -18,7 +17,7 @@ class ImportKnowledge(ApiHandler):
17
context = self.use_context(ctxid)
18
19
file_list = request.files.getlist("files[]")
21
- KNOWLEDGE_FOLDER = files.get_abs_path(memory.get_custom_knowledge_subdir_abs(context.agent0), "main")
20
+ KNOWLEDGE_FOLDER = files.get_abs_path(get_custom_knowledge_subdir_abs(context.agent0), "main")
21
22
# Ensure knowledge folder exists (create if missing)
23
try:
@@ -41,7 +40,7 @@ class ImportKnowledge(ApiHandler):
40
saved_filenames.append(filename)
41
42
#reload memory to re-import knowledge
44
- await memory.Memory.reload(context.agent0)
43
+ await Memory.reload(context.agent0)
44
context.log.set_initial_progress()
45
46
return {
plugins/memory/api/knowledge_path_get.py
renamed
+4
-7
@@ -1,9 +1,6 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, notification, projects, notification
3
-from python.helpers.plugins import import_plugin_module
4
-memory = import_plugin_module("memory", "helpers/memory.py")
5
-import os
6
-from werkzeug.utils import secure_filename
2
+from python.helpers import files, projects
3
+from plugins.memory.helpers.memory import get_custom_knowledge_subdir_abs
4
5
6
class GetKnowledgePath(ApiHandler):
@@ -17,11 +14,11 @@ class GetKnowledgePath(ApiHandler):
14
if project_name:
15
knowledge_folder = projects.get_project_meta_folder(project_name, "knowledge")
16
else:
20
- knowledge_folder = memory.get_custom_knowledge_subdir_abs(context.agent0)
17
+ knowledge_folder = get_custom_knowledge_subdir_abs(context.agent0)
18
19
knowledge_folder = files.normalize_a0_path(knowledge_folder)
20
21
return {
22
"ok": True,
23
"path": knowledge_folder,
27
- }
\ No newline at end of file
24
+ }
plugins/memory/api/knowledge_reindex.py
renamed
+2
-5
@@ -1,8 +1,5 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, notification, projects, notification
3
-from python.helpers.plugins import import_plugin_module
4
-memory = import_plugin_module("memory", "helpers/memory.py")
5
-import os
2
+from plugins.memory.helpers.memory import Memory
3
4
5
class ReindexKnowledge(ApiHandler):
@@ -13,7 +10,7 @@ class ReindexKnowledge(ApiHandler):
10
context = self.use_context(ctxid)
11
12
# reload memory to re-import knowledge
16
- await memory.Memory.reload(context.agent0)
13
+ await Memory.reload(context.agent0)
14
context.log.set_initial_progress()
15
16
return {
plugins/memory/extensions/backend/embedding_model_changed/_10_memory_reload.py
new
+10
@@ -0,0 +1,10 @@
1
+from python.helpers.extension import Extension
2
+
3
+# Direct import - this extension lives inside the memory plugin
4
+from plugins.memory.helpers.memory import reload as memory_reload
5
+
6
+
7
+class MemoryReload(Extension):
8
+
9
+ async def execute(self, **kwargs):
10
+ memory_reload()
plugins/memory/extensions/backend/message_loop_prompts_after/_50_recall_memories.py
renamed
+3
-8
@@ -3,14 +3,9 @@ from python.helpers.extension import Extension
3
from agent import LoopData
4
from python.helpers import dirty_json, errors, settings, log
5
6
-# Import Memory and DEFAULT_THRESHOLD from plugin
7
-import sys
8
-from pathlib import Path
9
-_plugin_root = Path(__file__).parent.parent.parent
10
-if str(_plugin_root) not in sys.path:
11
- sys.path.insert(0, str(_plugin_root))
12
-from helpers.memory import Memory
13
-from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
6
+# Direct import - this extension lives inside the memory plugin
7
+from plugins.memory.helpers.memory import Memory
8
+from plugins.memory.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
10
11
DATA_NAME_TASK = "_recall_memories_task"
plugins/memory/extensions/backend/message_loop_prompts_after/_91_recall_wait.py
renamed
+2
-9
@@ -1,7 +1,6 @@
1
from python.helpers.extension import Extension
2
from agent import LoopData
3
-from python.extensions.message_loop_prompts_after._50_recall_memories import DATA_NAME_TASK as DATA_NAME_TASK_MEMORIES, DATA_NAME_ITER as DATA_NAME_ITER_MEMORIES
4
-# from python.extensions.message_loop_prompts_after._51_recall_solutions import DATA_NAME_TASK as DATA_NAME_TASK_SOLUTIONS
3
+from plugins.memory.extensions.backend.message_loop_prompts_after._50_recall_memories import DATA_NAME_TASK as DATA_NAME_TASK_MEMORIES, DATA_NAME_ITER as DATA_NAME_ITER_MEMORIES
4
from python.helpers import settings
5
6
class RecallWait(Extension):
@@ -21,12 +20,6 @@ class RecallWait(Extension):
20
delay_text = self.agent.read_prompt("memory.recall_delay_msg.md")
21
loop_data.extras_temporary["memory_recall_delayed"] = delay_text
22
return
24
-
23
+
24
# otherwise await the task
25
await task
27
-
28
- # task = self.agent.get_data(DATA_NAME_TASK_SOLUTIONS)
29
- # if task and not task.done():
30
- # # self.agent.context.log.set_progress("Recalling solutions...")
31
- # await task
32
-
plugins/memory/extensions/backend/monologue_end/_50_memorize_fragments.py
renamed
+4
-10
@@ -6,14 +6,9 @@ from agent import LoopData
6
from python.helpers.log import LogItem
7
from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
8
9
-# Import Memory and DEFAULT_THRESHOLD from plugin
10
-import sys
11
-from pathlib import Path
12
-_plugin_root = Path(__file__).parent.parent.parent
13
-if str(_plugin_root) not in sys.path:
14
- sys.path.insert(0, str(_plugin_root))
15
-from helpers.memory import Memory
16
-from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
+# Direct import - this extension lives inside the memory plugin
10
+from plugins.memory.helpers.memory import Memory
11
+from plugins.memory.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
12
13
14
class MemorizeMemories(Extension):
@@ -115,7 +110,7 @@ class MemorizeMemories(Extension):
110
111
try:
112
# Use intelligent consolidation system
118
- from helpers.memory_consolidation import create_memory_consolidator
113
+ from plugins.memory.helpers.memory_consolidation import create_memory_consolidator
114
consolidator = create_memory_consolidator(
115
self.agent,
116
similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
@@ -197,7 +192,6 @@ class MemorizeMemories(Extension):
192
193
194
200
-
195
except Exception as e:
196
err = errors.format_error(e)
197
self.agent.context.log.log(
plugins/memory/extensions/backend/monologue_end/_51_memorize_solutions.py
renamed
+4
-9
@@ -6,14 +6,9 @@ from agent import LoopData
6
from python.helpers.log import LogItem
7
from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
8
9
-# Import Memory and DEFAULT_THRESHOLD from plugin
10
-import sys
11
-from pathlib import Path
12
-_plugin_root = Path(__file__).parent.parent.parent
13
-if str(_plugin_root) not in sys.path:
14
- sys.path.insert(0, str(_plugin_root))
15
-from helpers.memory import Memory
16
-from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
+# Direct import - this extension lives inside the memory plugin
10
+from plugins.memory.helpers.memory import Memory
11
+from plugins.memory.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
12
13
class MemorizeSolutions(Extension):
14
@@ -122,7 +117,7 @@ class MemorizeSolutions(Extension):
117
if set["memory_memorize_consolidation"]:
118
try:
119
# Use intelligent consolidation system
125
- from helpers.memory_consolidation import create_memory_consolidator
120
+ from plugins.memory.helpers.memory_consolidation import create_memory_consolidator
121
consolidator = create_memory_consolidator(
122
self.agent,
123
similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
plugins/memory/extensions/backend/monologue_start/_10_memory_init.py
new
+11
@@ -0,0 +1,11 @@
1
+from python.helpers.extension import Extension
2
+from agent import LoopData
3
+
4
+# Direct import - this extension lives inside the memory plugin
5
+from plugins.memory.helpers import memory
6
+
7
+
8
+class MemoryInit(Extension):
9
+
10
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
11
+ db = await memory.Memory.get(self.agent)
plugins/memory/extensions/backend/system_prompt/_20_behaviour_prompt.py
renamed
+6
-6
@@ -1,27 +1,27 @@
1
-from datetime import datetime
1
from python.helpers.extension import Extension
2
from agent import Agent, LoopData
3
from python.helpers import files
5
-from python.helpers.plugins import import_plugin_module
4
7
-memory = import_plugin_module("memory", "helpers/memory.py")
5
+# Direct import - this extension lives inside the memory plugin
6
+from plugins.memory.helpers import memory
7
8
9
class BehaviourPrompt(Extension):
10
11
async def execute(self, system_prompt: list[str]=[], loop_data: LoopData = LoopData(), **kwargs):
12
prompt = read_rules(self.agent)
14
- system_prompt.insert(0, prompt) #.append(prompt)
13
+ system_prompt.insert(0, prompt)
14
+
15
16
def get_custom_rules_file(agent: Agent):
17
return files.get_abs_path(memory.get_memory_subdir_abs(agent), "behaviour.md")
18
19
+
20
def read_rules(agent: Agent):
21
rules_file = get_custom_rules_file(agent)
22
if files.exists(rules_file):
22
- rules = files.read_file(rules_file) # no includes and vars here, that could crash
23
+ rules = files.read_file(rules_file)
24
return agent.read_prompt("agent.system.behaviour.md", rules=rules)
25
else:
26
rules = agent.read_prompt("agent.system.behaviour_default.md")
27
return agent.read_prompt("agent.system.behaviour.md", rules=rules)
27
-
\ No newline at end of file
plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard-store.js
renamed
+9
-8
@@ -3,6 +3,7 @@ import { getContext } from "/index.js";
3
import * as API from "/js/api.js";
4
import { openModal, closeModal } from "/js/modals.js";
5
import { store as notificationStore } from "/components/notifications/notification-store.js";
6
+const MEMORY_DASHBOARD_API = "/plugins/memory/memory_dashboard";
7
8
// Helper function for toasts
9
function justToast(text, type = "info", timeout = 5000) {
@@ -53,7 +54,7 @@ const memoryDashboardStore = {
54
pollingEnabled: false,
55
56
async openModal() {
56
- await openModal("../plugins/memory/ui/memory-dashboard.html");
57
+ await openModal("../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html");
58
},
59
60
init() {
@@ -91,7 +92,7 @@ const memoryDashboardStore = {
92
async getCurrentMemorySubdir() {
93
try {
94
// Try to get current memory subdirectory from the backend
94
- const response = await API.callJsonApi("memory_dashboard", {
95
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
96
action: "get_current_memory_subdir",
97
context_id: getContext(),
98
});
@@ -113,7 +114,7 @@ const memoryDashboardStore = {
114
this.error = null;
115
116
try {
116
- const response = await API.callJsonApi("memory_dashboard", {
117
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
118
action: "get_memory_subdirs",
119
});
120
@@ -172,7 +173,7 @@ const memoryDashboardStore = {
173
}
174
175
try {
175
- const response = await API.callJsonApi("memory_dashboard", {
176
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
177
action: "search",
178
memory_subdir: this.selectedMemorySubdir,
179
area: this.areaFilter,
@@ -326,7 +327,7 @@ const memoryDashboardStore = {
327
328
try {
329
this.loading = true;
329
- const response = await API.callJsonApi("memory_dashboard", {
330
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
331
action: "bulk_delete",
332
memory_subdir: this.selectedMemorySubdir,
333
memory_ids: selectedMemories.map((memory) => memory.id),
@@ -448,7 +449,7 @@ ${memory.content_full}
449
this.editMode = false;
450
this.editMemoryBackup = null;
451
// Use global modal system
451
- openModal("../plugins/memory/ui/memory-detail-modal.html");
452
+ openModal("../plugins/memory/extensions/frontend/memory_dashboard/memory-detail-modal.html");
453
},
454
455
closeMemoryDetails() {
@@ -556,7 +557,7 @@ ${memory.content_full}
557
const isViewingThisMemory =
558
this.detailMemory && this.detailMemory.id === memory.id;
559
559
- const response = await API.callJsonApi("memory_dashboard", {
560
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
561
action: "delete",
562
memory_subdir: this.selectedMemorySubdir,
563
memory_id: memory.id,
@@ -674,7 +675,7 @@ ${memory.content_full}
675
async confirmEditMode() {
676
try {
677
677
- const response = await API.callJsonApi("memory_dashboard", {
678
+ const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
679
action: "update",
680
memory_subdir: this.selectedMemorySubdir,
681
original: JSON.parse(this.editMemoryBackup),
plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html
renamed
+1
-1
@@ -3,7 +3,7 @@
3
<head>
4
<title>Memory Dashboard</title>
5
<script type="module">
6
- import { store } from "/plugins/memory/ui/memory-dashboard-store.js";
6
+ import { store } from "/plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard-store.js";
7
</script>
8
</head>
9
plugins/memory/extensions/frontend/memory_dashboard/memory-detail-modal.html
renamed
plugins/memory/extensions/frontend/sidebar.quick_actions.dropdown/memory-entry.html
new
+14
@@ -0,0 +1,14 @@
1
+<html>
2
+
3
+<head>
4
+ <meta name="plugin-target" content=".quick-actions-dropdown">
5
+</head>
6
+
7
+<body>
8
+ <button class="dropdown-item" @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html'); dropdownOpen = false">
9
+ <span class="material-symbols-outlined">psychology</span>
10
+ <span>Memories</span>
11
+ </button>
12
+</body>
13
+
14
+</html>
plugins/memory/extensions/monologue_start/_10_memory_init.py
deleted
-20
@@ -1,20 +0,0 @@
1
-from python.helpers.extension import Extension
2
-from agent import LoopData
3
-import asyncio
4
-
5
-# Import memory from plugin
6
-import sys
7
-from pathlib import Path
8
-_plugin_root = Path(__file__).parent.parent.parent
9
-if str(_plugin_root) not in sys.path:
10
- sys.path.insert(0, str(_plugin_root))
11
-from helpers import memory
12
-
13
-
14
-class MemoryInit(Extension):
15
-
16
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
17
- db = await memory.Memory.get(self.agent)
18
-
19
-
20
-
\ No newline at end of file
plugins/memory/plugin.json
deleted
-151
@@ -1,151 +0,0 @@
1
-{
2
- "id": "memory",
3
- "name": "Memory System",
4
- "version": "1.0.0",
5
- "author": "Agent Zero Team",
6
- "description": "FAISS-based vector memory with embeddings caching and knowledge preloading",
7
- "tags": ["memory", "vector", "faiss", "core"],
8
- "provides": {
9
- "helper": [
10
- {
11
- "module": "helpers/memory.py",
12
- "description": "Core Memory class with FAISS vector store"
13
- },
14
- {
15
- "module": "helpers/memory_consolidation.py",
16
- "description": "Intelligent memory deduplication"
17
- },
18
- {
19
- "module": "helpers/knowledge_import.py",
20
- "description": "Knowledge file ingestion pipeline"
21
- }
22
- ],
23
- "tool": [
24
- {
25
- "module": "tools/memory_save.py",
26
- "description": "Save memories"
27
- },
28
- {
29
- "module": "tools/memory_load.py",
30
- "description": "Load/search memories"
31
- },
32
- {
33
- "module": "tools/memory_delete.py",
34
- "description": "Delete specific memories"
35
- },
36
- {
37
- "module": "tools/memory_forget.py",
38
- "description": "Forget memories by query"
39
- }
40
- ],
41
- "extension": [
42
- {
43
- "module": "extensions/monologue_start/_10_memory_init.py",
44
- "description": "Initialize memory on monologue start"
45
- },
46
- {
47
- "module": "extensions/monologue_end/_50_memorize_fragments.py",
48
- "description": "Auto-memorize conversation fragments"
49
- },
50
- {
51
- "module": "extensions/monologue_end/_51_memorize_solutions.py",
52
- "description": "Auto-memorize solutions"
53
- },
54
- {
55
- "module": "extensions/message_loop_prompts_after/_50_recall_memories.py",
56
- "description": "Recall relevant memories into prompt"
57
- }
58
- ],
59
- "api": [
60
- {
61
- "module": "api/memory_dashboard.py",
62
- "description": "Memory browser and management dashboard"
63
- }
64
- ],
65
- "ui": [
66
- {
67
- "component": "ui/memory-dashboard.html",
68
- "module": "ui/memory-dashboard-store.js",
69
- "description": "Memory Dashboard UI"
70
- },
71
- {
72
- "component": "ui/memory-detail-modal.html",
73
- "description": "Memory Detail Modal"
74
- }
75
- ],
76
- "prompt": [
77
- {
78
- "module": "prompts/agent.system.memories.md",
79
- "description": "Agent system prompt for memories"
80
- },
81
- {
82
- "module": "prompts/agent.system.solutions.md",
83
- "description": "Agent system prompt for solutions"
84
- },
85
- {
86
- "module": "prompts/agent.system.tool.memory.md",
87
- "description": "Memory tool system prompt"
88
- },
89
- {
90
- "module": "prompts/fw.memory.hist_suc.sys.md",
91
- "description": "Framework memory history success prompt"
92
- },
93
- {
94
- "module": "prompts/fw.memory.hist_sum.sys.md",
95
- "description": "Framework memory history summary prompt"
96
- },
97
- {
98
- "module": "prompts/fw.memory_saved.md",
99
- "description": "Framework memory saved prompt"
100
- },
101
- {
102
- "module": "prompts/memory.consolidation.msg.md",
103
- "description": "Memory consolidation message"
104
- },
105
- {
106
- "module": "prompts/memory.consolidation.sys.md",
107
- "description": "Memory consolidation system prompt"
108
- },
109
- {
110
- "module": "prompts/memory.keyword_extraction.msg.md",
111
- "description": "Keyword extraction message"
112
- },
113
- {
114
- "module": "prompts/memory.keyword_extraction.sys.md",
115
- "description": "Keyword extraction system prompt"
116
- },
117
- {
118
- "module": "prompts/memory.memories_filter.msg.md",
119
- "description": "Memories filter message"
120
- },
121
- {
122
- "module": "prompts/memory.memories_filter.sys.md",
123
- "description": "Memories filter system prompt"
124
- },
125
- {
126
- "module": "prompts/memory.memories_query.msg.md",
127
- "description": "Memories query message"
128
- },
129
- {
130
- "module": "prompts/memory.memories_query.sys.md",
131
- "description": "Memory query system prompt"
132
- },
133
- {
134
- "module": "prompts/memory.memories_sum.sys.md",
135
- "description": "Memories summary system prompt"
136
- },
137
- {
138
- "module": "prompts/memory.recall_delay_msg.md",
139
- "description": "Recall delay message"
140
- },
141
- {
142
- "module": "prompts/memory.solutions_query.sys.md",
143
- "description": "Solutions query system prompt"
144
- },
145
- {
146
- "module": "prompts/memory.solutions_sum.sys.md",
147
- "description": "Solutions summary system prompt"
148
- }
149
- ]
150
- }
151
-}
plugins/memory/tools/behaviour_adjustment.py
renamed
+3
-8
@@ -1,10 +1,11 @@
1
from python.helpers import files
2
-from python.helpers.plugins import import_plugin_module
3
-memory = import_plugin_module("memory", "helpers/memory.py")
2
from python.helpers.tool import Tool, Response
3
from agent import Agent
4
from python.helpers.log import LogItem
5
6
+# Direct import - this tool lives inside the memory plugin
7
+from plugins.memory.helpers import memory
8
+
9
10
class UpdateBehaviour(Tool):
11
@@ -19,12 +20,6 @@ class UpdateBehaviour(Tool):
20
message=self.agent.read_prompt("behaviour.updated.md"), break_loop=False
21
)
22
22
- # async def before_execution(self, **kwargs):
23
- # pass
24
-
25
- # async def after_execution(self, response, **kwargs):
26
- # pass
27
-
23
24
async def update_behaviour(agent: Agent, log_item: LogItem, adjustments: str):
25
python/api/chat_files_path_get.py
+1
-4
@@ -1,8 +1,5 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, notification, projects, notification, runtime, settings
3
-from python.helpers.plugins import import_plugin_module
4
-memory = import_plugin_module("memory", "helpers/memory.py")
5
-import os
2
+from python.helpers import files, projects, settings
3
4
5
class GetChatFilesPath(ApiHandler):
python/api/plugins_list.py
deleted
-22
@@ -1,22 +0,0 @@
1
-from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import plugins
3
-
4
-
5
-class PluginsList(ApiHandler):
6
- """
7
- API handler for listing all available plugins.
8
- Returns a list of plugin manifests.
9
- """
10
-
11
- @classmethod
12
- def get_methods(cls):
13
- return ["GET", "POST"]
14
-
15
- async def process(self, input: dict, request: Request) -> dict | Response:
16
- # Get all available plugins
17
- plugin_list = plugins.list_plugins()
18
-
19
- # Serialize plugin objects using helper
20
- data = [plugins.build_plugin_response_data(p) for p in plugin_list]
21
-
22
- return {"ok": True, "data": data}
python/api/plugins_resolve.py
+5
-37
@@ -4,11 +4,9 @@ from python.helpers import plugins
4
5
class PluginsResolve(ApiHandler):
6
"""
7
- API handler for resolving plugin manifests.
8
- Accepts a plugin ID or list of IDs and returns normalized manifest(s) with URLs and props.
9
-
10
- Single ID: {"id": "example"} -> {"ok": True, "data": {...}}
11
- Multiple IDs: {"ids": ["example", "another"]} -> {"ok": True, "data": [{...}, {...}]}
7
+ Return all injectable plugin frontend components.
8
+ Each plugin's extensions/frontend/**/*.html files are returned.
9
+ The components themselves declare their injection target via <meta> tags.
10
"""
11
12
@classmethod
@@ -16,35 +14,5 @@ class PluginsResolve(ApiHandler):
14
return ["POST"]
15
16
async def process(self, input: dict, request: Request) -> dict | Response:
19
- # Support both single ID and array of IDs
20
- plugin_id = input.get("id")
21
- plugin_ids = input.get("ids")
22
-
23
- # Batch mode: array of IDs
24
- if plugin_ids:
25
- if not isinstance(plugin_ids, list):
26
- return {"ok": False, "error": "ids must be an array"}
27
-
28
- results = []
29
- for pid in plugin_ids:
30
- plugin = plugins.find_plugin(pid)
31
- if plugin:
32
- results.append(plugins.build_plugin_response_data(plugin))
33
- else:
34
- results.append({
35
- "id": pid,
36
- "error": f"Plugin '{pid}' not found or invalid manifest"
37
- })
38
-
39
- return {"ok": True, "data": results}
40
-
41
- # Single mode: one ID
42
- elif plugin_id:
43
- plugin = plugins.find_plugin(plugin_id)
44
- if not plugin:
45
- return {"ok": False, "error": f"Plugin '{plugin_id}' not found or invalid manifest"}
46
-
47
- return {"ok": True, "data": plugins.build_plugin_response_data(plugin)}
48
-
49
- else:
50
- return {"ok": False, "error": "Missing plugin ID or IDs"}
17
+ data = plugins.get_frontend_components()
18
+ return {"ok": True, "data": data}
python/extensions/message_loop_prompts_after/_50_recall_memories.py
deleted
-8
@@ -1,8 +0,0 @@
1
-"""Recall memories extension - implementation provided by the memory plugin."""
2
-from python.helpers.plugins import import_plugin_module
3
-
4
-# Import the actual implementation from the plugin
5
-_mod = import_plugin_module("memory", "extensions/message_loop_prompts_after/_50_recall_memories.py")
6
-
7
-# Re-export all public names
8
-globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_end/_50_memorize_fragments.py
deleted
-8
@@ -1,8 +0,0 @@
1
-"""Memorize fragments extension - implementation provided by the memory plugin."""
2
-from python.helpers.plugins import import_plugin_module
3
-
4
-# Import the actual implementation from the plugin
5
-_mod = import_plugin_module("memory", "extensions/monologue_end/_50_memorize_fragments.py")
6
-
7
-# Re-export all public names
8
-globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_end/_51_memorize_solutions.py
deleted
-8
@@ -1,8 +0,0 @@
1
-"""Memorize solutions extension - implementation provided by the memory plugin."""
2
-from python.helpers.plugins import import_plugin_module
3
-
4
-# Import the actual implementation from the plugin
5
-_mod = import_plugin_module("memory", "extensions/monologue_end/_51_memorize_solutions.py")
6
-
7
-# Re-export all public names
8
-globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_start/_10_memory_init.py
deleted
-8
@@ -1,8 +0,0 @@
1
-"""Memory initialization extension - implementation provided by the memory plugin."""
2
-from python.helpers.plugins import import_plugin_module
3
-
4
-# Import the actual implementation from the plugin
5
-_mod = import_plugin_module("memory", "extensions/monologue_start/_10_memory_init.py")
6
-
7
-# Re-export all public names
8
-globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/helpers/extension.py
+5
-5
@@ -31,11 +31,11 @@ async def call_extensions(
31
32
# search for extension folders in all agent's paths
33
paths = subagents.get_paths(agent, "extensions", extension_point, default_root="python")
34
-
35
- # Add plugin extension paths
36
- plugin_paths = plugins.get_plugin_paths("extension", extension_point)
37
- paths.extend(plugin_paths)
38
-
34
+
35
+ # Add plugin backend extension paths (plugins/*/extensions/backend/{extension_point})
36
+ plugin_paths = plugins.get_plugin_paths("extensions", "backend", extension_point)
37
+ paths.extend(p for p in plugin_paths if p not in paths)
38
+
39
all_exts = [cls for path in paths for cls in _get_extensions(path)]
40
41
# merge: first ocurrence of file name is the override
python/helpers/plugins.py
+62
-229
@@ -1,272 +1,105 @@
1
from __future__ import annotations
2
3
-import json
4
-from dataclasses import dataclass, field
3
+import re
4
+from dataclasses import dataclass
5
from pathlib import Path
6
from typing import Any, Dict, List, Optional
7
8
from python.helpers import files
9
10
+# Extracts target selector from <meta name="plugin-target" content="...">
11
+_META_TARGET_RE = re.compile(
12
+ r'<meta\s+name=["\']plugin-target["\']\s+content=["\']([^"\']+)["\']',
13
+ re.IGNORECASE,
14
+)
15
11
-# ============================================================================
12
-# Core Data Structures
13
-# ============================================================================
16
17
@dataclass(slots=True)
18
class Plugin:
19
id: str
20
name: str
21
path: Path
20
- manifest_path: Path
21
- provides: Dict[str, Any] = field(default_factory=dict) # capability map
22
- version: str = ""
23
- author: str = ""
24
- description: str = ""
25
- tags: List[str] = field(default_factory=list)
26
-
27
- # Optional heavy fields
28
- raw_manifest: Dict[str, Any] = field(default_factory=dict)
22
23
31
-# ============================================================================
32
-# Discovery & Loading
33
-# ============================================================================
34
-
24
def get_plugin_roots() -> List[str]:
36
- """
37
- Get plugin root directories.
38
- Priority: project plugins > usr plugins > core plugins
39
- """
40
- # Project-specific plugins
41
- projects = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/plugins")
42
-
25
+ """Plugin root directories, ordered by priority (user first)."""
26
+ # Project-specific plugins (commented out for now, will add project/agent plugins later)
27
+ # projects = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/plugins")
28
return [
44
- *projects,
29
+ # *projects,
30
files.get_abs_path("usr/plugins"),
31
files.get_abs_path("plugins"),
32
]
33
34
50
-def discover_plugin_manifests(root: str) -> List[Path]:
51
- """Recursively discover plugin.json files under a root directory."""
52
- root_path = Path(root)
53
- if not root_path.exists():
54
- return []
55
-
56
- results: List[Path] = []
57
- for p in root_path.rglob("plugin.json"):
58
- if p.is_file() and ".git" not in str(p):
59
- results.append(p)
60
-
61
- return sorted(results, key=lambda x: str(x))
62
-
63
-
64
-def plugin_from_manifest(manifest_path: Path) -> Optional[Plugin]:
65
- """Load plugin from manifest file."""
66
- try:
67
- text = manifest_path.read_text(encoding="utf-8", errors="replace")
68
- manifest = json.loads(text)
69
- except Exception:
70
- return None
71
-
72
- if not isinstance(manifest, dict):
73
- return None
74
-
75
- plugin_dir = manifest_path.parent
76
- plugin_id = manifest.get("id", "").strip()
77
-
78
- if not plugin_id:
79
- return None
80
-
81
- plugin = Plugin(
82
- id=plugin_id,
83
- name=manifest.get("name", plugin_id),
84
- path=plugin_dir,
85
- manifest_path=manifest_path,
86
- provides=manifest.get("provides", {}),
87
- version=manifest.get("version", ""),
88
- author=manifest.get("author", ""),
89
- description=manifest.get("description", ""),
90
- tags=manifest.get("tags", []),
91
- raw_manifest=manifest,
92
- )
93
-
94
- return plugin
95
-
96
-
97
-# ============================================================================
98
-# Registry Access (List/Find)
99
-# ============================================================================
100
-
35
def list_plugins() -> List[Plugin]:
102
- """List all discovered plugins."""
103
- plugins: List[Plugin] = []
104
-
105
- for root in get_plugin_roots():
106
- for manifest_path in discover_plugin_manifests(root):
107
- p = plugin_from_manifest(manifest_path)
108
- if p:
109
- plugins.append(p)
110
-
111
- # Dedupe by ID (earlier roots win)
36
+ """Discover plugins by directory convention. First root wins on ID conflict."""
37
by_id: Dict[str, Plugin] = {}
113
- for p in plugins:
114
- if p.id not in by_id:
115
- by_id[p.id] = p
116
-
38
+ for root in get_plugin_roots():
39
+ root_path = Path(root)
40
+ if not root_path.exists():
41
+ continue
42
+ for d in sorted(root_path.iterdir(), key=lambda p: p.name):
43
+ if not d.is_dir() or d.name.startswith("."):
44
+ continue
45
+ if d.name not in by_id:
46
+ by_id[d.name] = Plugin(id=d.name, name=d.name, path=d)
47
return list(by_id.values())
48
49
50
def find_plugin(plugin_id: str) -> Optional[Plugin]:
121
- """Find a specific plugin by ID."""
51
+ """Find a single plugin by ID."""
52
if not plugin_id:
53
return None
124
-
125
- # Search roots in priority order (first one found wins)
126
- for root in get_plugin_roots():
127
- for manifest_path in discover_plugin_manifests(root):
128
- p = plugin_from_manifest(manifest_path)
129
- if p and p.id == plugin_id:
130
- return p
131
-
54
+ for p in list_plugins():
55
+ if p.id == plugin_id:
56
+ return p
57
return None
58
59
135
-# ============================================================================
136
-# Path Resolution & Import
137
-# ============================================================================
138
-
139
-def _extract_module_dir(module_path: str) -> str:
140
- """Extract directory from module path (e.g., 'tools/my_tool.py' -> 'tools')."""
141
- return str(Path(module_path).parent)
142
-
143
-
144
-def get_plugin_paths(cap_type: str, *subpaths: str) -> List[str]:
60
+def get_plugin_paths(*subpaths: str) -> List[str]:
61
"""
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:
150
- cap_type: Capability type (e.g., "tool", "extension", "api", "agent")
151
- subpaths: Additional path components to append
152
-
153
- Returns:
154
- List of absolute paths matching the capability type
62
+ Resolve existing directories under each plugin matching subpaths.
63
+
64
+ Example:
65
+ get_plugin_paths("extensions", "backend", "monologue_end")
66
+ -> ["/abs/plugins/memory/extensions/backend/monologue_end", ...]
67
"""
68
+ sub = "/".join(subpaths) if subpaths else ""
69
paths: List[str] = []
157
-
70
for plugin in list_plugins():
159
- cap_config = plugin.provides.get(cap_type)
160
- if not cap_config:
161
- continue
162
-
163
- # Normalize to list for uniform processing
164
- items = []
165
- if isinstance(cap_config, list):
166
- items = [c for c in cap_config if isinstance(c, dict)]
167
- elif isinstance(cap_config, dict):
168
- items = [cap_config]
169
-
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
-
71
+ candidate = str(plugin.path / sub) if sub else str(plugin.path)
72
+ if Path(candidate).is_dir() and candidate not in paths:
73
+ paths.append(candidate)
74
return paths
75
76
189
-def import_plugin_module(plugin_id: str, module_path: str) -> Any:
77
+def get_frontend_components() -> List[Dict[str, Any]]:
78
"""
191
- Import a Python module from a plugin using importlib.
192
-
193
- Args:
194
- plugin_id: Plugin ID
195
- module_path: Relative path to module within plugin (e.g., "helpers/memory.py")
196
-
197
- Returns:
198
- The imported module object
199
-
200
- Raises:
201
- ImportError: If plugin or module not found
79
+ Return all injectable plugin frontend components.
80
+ Convention: plugins/*/extensions/frontend/**/*.html
81
+ The backend reads each file to extract the optional
82
+ <meta name="plugin-target" content=".css-selector"> tag so the
83
+ frontend never needs to fetch component HTML just to discover targets.
84
"""
203
- import importlib.util
204
- import sys
205
-
206
- plugin = find_plugin(plugin_id)
207
- if not plugin:
208
- raise ImportError(f"Plugin '{plugin_id}' not found")
209
-
210
- full_path = plugin.path / module_path
211
- if not full_path.exists():
212
- raise ImportError(f"Module '{module_path}' not found in plugin '{plugin_id}'")
213
-
214
- # Create a unique module name to avoid conflicts
215
- module_name = f"plugins.{plugin_id}.{module_path.replace('/', '.').replace('.py', '')}"
216
-
217
- # Check if already loaded
218
- if module_name in sys.modules:
219
- return sys.modules[module_name]
220
-
221
- # Load the module
222
- spec = importlib.util.spec_from_file_location(module_name, full_path)
223
- if spec is None or spec.loader is None:
224
- raise ImportError(f"Could not load module spec from {full_path}")
225
-
226
- module = importlib.util.module_from_spec(spec)
227
- sys.modules[module_name] = module
228
- spec.loader.exec_module(module)
229
-
230
- return module
231
-
232
-
233
-# ============================================================================
234
-# API Helpers
235
-# ============================================================================
236
-
237
-def build_plugin_response_data(plugin: Plugin) -> dict:
238
- """
239
- Build normalized API response data for a plugin.
240
- Resolves URLs for UI capabilities.
241
-
242
- Args:
243
- plugin: Plugin object to serialize
244
-
245
- Returns:
246
- Dictionary with plugin data, component_url, module_url, and provides
247
- """
248
- base_url = f"/plugins/{plugin.id}/"
249
-
250
- response_data = {
251
- "id": plugin.id,
252
- "name": plugin.name,
253
- "base_url": base_url,
254
- "version": plugin.version,
255
- "author": plugin.author,
256
- "description": plugin.description,
257
- "tags": plugin.tags,
258
- "provides": plugin.provides,
259
- }
260
-
261
- # Resolve UI capability URLs from provides.ui
262
- if "ui" in plugin.provides:
263
- ui_config = plugin.provides["ui"]
264
- if isinstance(ui_config, dict):
265
- if ui_config.get("component"):
266
- response_data["component_url"] = f"{base_url}{ui_config['component']}"
267
- if ui_config.get("module"):
268
- response_data["module_url"] = f"{base_url}{ui_config['module']}"
269
- if ui_config.get("props"):
270
- response_data["props"] = ui_config["props"]
271
-
272
- return response_data
85
+ entries: List[Dict[str, Any]] = []
86
+ for plugin in list_plugins():
87
+ frontend_dir = plugin.path / "extensions" / "frontend"
88
+ if not frontend_dir.is_dir():
89
+ continue
90
+ for html_file in sorted(frontend_dir.rglob("*.html"), key=lambda p: p.name):
91
+ rel_path = html_file.relative_to(plugin.path).as_posix()
92
+ entry: Dict[str, Any] = {
93
+ "plugin_id": plugin.id,
94
+ "component_url": f"/plugins/{plugin.id}/{rel_path}",
95
+ }
96
+ # Extract injection target from meta tag (if present)
97
+ try:
98
+ content = html_file.read_text(encoding="utf-8")
99
+ m = _META_TARGET_RE.search(content)
100
+ if m:
101
+ entry["target"] = m.group(1)
102
+ except Exception:
103
+ pass
104
+ entries.append(entry)
105
+ return entries
python/helpers/projects.py
+1
-8
@@ -477,15 +477,8 @@ def create_project_meta_folders(name: str):
477
# create instructions folder
478
files.create_dir(get_project_meta_folder(name, PROJECT_INSTRUCTIONS_DIR))
479
480
- # create knowledge folders
480
+ # create knowledge folders (plugins create their own subdirs lazily)
481
files.create_dir(get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR))
482
- from python.helpers.plugins import import_plugin_module
483
- memory = import_plugin_module("memory", "helpers/memory.py")
484
-
485
- for memory_type in memory.Memory.Area:
486
- files.create_dir(
487
- get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR, memory_type.value)
488
- )
482
483
484
def get_knowledge_files_count(name: str):
python/helpers/settings.py
+5
-5
@@ -623,17 +623,17 @@ def _apply_settings(previous: Settings | None):
623
whisper.preload, _settings["stt_model_size"]
624
) # TODO overkill, replace with background task
625
626
- # force memory reload on embedding model change
626
+ # notify plugins of embedding model change
627
if not previous or (
628
_settings["embed_model_name"] != previous["embed_model_name"]
629
or _settings["embed_model_provider"] != previous["embed_model_provider"]
630
or _settings["embed_model_kwargs"] != previous["embed_model_kwargs"]
631
):
632
- from python.helpers.plugins import import_plugin_module
633
- memory = import_plugin_module("memory", "helpers/memory.py")
634
- memory_reload = memory.reload
632
+ from python.helpers.extension import call_extensions
633
636
- memory_reload()
634
+ defer.DeferredTask().start_task(
635
+ call_extensions, "embedding_model_changed"
636
+ )
637
638
# update mcp settings if necessary
639
if not previous or _settings["mcp_servers"] != previous["mcp_servers"]:
python/helpers/subagents.py
+15
-7
@@ -57,13 +57,14 @@ def get_agents_dict(
57
)
58
return merged
59
60
+ from python.helpers import plugins
61
+
62
# load default, plugin, and custom agents and merge
63
default_agents = _get_agents_list_from_dir(DEFAULT_AGENTS_DIR, origin="default")
64
merged: dict[str, SubAgentListItem] = dict(default_agents)
65
66
# merge with plugin agents
65
- from python.helpers import plugins
66
- for plugin_dir in reversed(plugins.get_plugin_paths("agent")):
67
+ for plugin_dir in plugins.get_plugin_paths("agents"):
68
plugin_agents = _get_agents_list_from_dir(plugin_dir, origin="plugin")
69
merged = _merge_agent_dicts(merged, plugin_agents)
70
@@ -110,6 +111,8 @@ def load_agent_data(name: str, project_name: str | None = None) -> SubAgent:
111
return original
112
return override
113
114
+ from python.helpers import plugins
115
+
116
# load default, plugin, and user agents and merge
117
default_agent = _load_agent_data_from_dir(
118
DEFAULT_AGENTS_DIR, name, origin="default"
@@ -117,8 +120,7 @@ def load_agent_data(name: str, project_name: str | None = None) -> SubAgent:
120
merged = default_agent
121
122
# merge with plugin agents
120
- from python.helpers import plugins
121
- for plugin_dir in reversed(plugins.get_plugin_paths("agent")):
123
+ for plugin_dir in plugins.get_plugin_paths("agents"):
124
plugin_agent = _load_agent_data_from_dir(plugin_dir, name, origin="plugin")
125
merged = _merge_agent(merged, plugin_agent)
126
@@ -240,7 +242,7 @@ def _merge_agent_list_items(
242
def get_agents_roots() -> list[str]:
243
from python.helpers import plugins
244
243
- plugin_agents = list(reversed(plugins.get_plugin_paths("agent")))
245
+ plugin_agents = plugins.get_plugin_paths("agents")
246
project_agents = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/agents")
247
paths = [
248
files.get_abs_path(DEFAULT_AGENTS_DIR),
@@ -326,10 +328,11 @@ def get_paths(
328
include_project: bool = True,
329
include_user: bool = True,
330
include_default: bool = True,
331
+ include_plugins: bool = False,
332
default_root: str = "",
333
) -> list[str]:
334
"""Returns list of file paths for the given agent and subpaths, searched in order of priority:
332
- project/agents/, project/, usr/agents/, agents/, usr/, default."""
335
+ project/agents/, project/, usr/agents/, plugin agents/, agents/, usr/, plugins/, default."""
336
paths: list[str] = []
337
check_subpaths = subpaths if must_exist_completely else []
338
profile_name = agent.config.profile if agent and agent.config.profile else ""
@@ -363,7 +366,7 @@ def get_paths(
366
367
# plugin agents/<profile>/...
368
from python.helpers import plugins
366
- for plugin_dir in plugins.get_plugin_paths("agent"):
369
+ for plugin_dir in plugins.get_plugin_paths("agents"):
370
path = files.get_abs_path(plugin_dir, profile_name, *subpaths)
371
if (not must_exist_completely) or files.exists(files.get_abs_path(plugin_dir, profile_name, *check_subpaths)):
372
paths.append(path)
@@ -379,6 +382,11 @@ def get_paths(
382
if (not must_exist_completely) or files.exists(path):
383
paths.append(path)
384
385
+ if include_plugins:
386
+ # plugins/*/subpaths...
387
+ plugin_paths = plugins.get_plugin_paths(*subpaths)
388
+ paths.extend(p for p in plugin_paths if p not in paths)
389
+
390
if include_default:
391
# default_root/...
392
path = files.get_abs_path(default_root, *subpaths)
python/tools/knowledge_tool._py
+5
-7
@@ -1,9 +1,7 @@
1
import asyncio
2
from python.helpers import dotenv, perplexity_search, duckduckgo_search
3
-from python.helpers.plugins import import_plugin_module
4
-memory = import_plugin_module("memory", "helpers/memory.py")
5
-memory_load_tool = import_plugin_module("memory", "tools/memory_load.py")
6
-DEFAULT_MEMORY_THRESHOLD = memory_load_tool.DEFAULT_THRESHOLD
3
+from plugins.memory.helpers.memory import Memory
4
+from plugins.memory.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
5
6
from python.helpers.tool import Tool, Response
7
from python.helpers.document_query import DocumentQueryHelper
@@ -98,11 +96,11 @@ class Knowledge(Tool):
96
return result
97
98
async def mem_search(self, question: str):
101
- db = await memory.Memory.get(self.agent)
99
+ db = await Memory.get(self.agent)
100
docs = await db.search_similarity_threshold(
101
query=question, limit=5, threshold=DEFAULT_MEMORY_THRESHOLD
102
)
105
- text = memory.Memory.format_docs_plain(docs)
103
+ text = Memory.format_docs_plain(docs)
104
return "\n\n".join(text)
105
106
async def mem_search_enhanced(self, question: str):
@@ -111,7 +109,7 @@ class Knowledge(Tool):
109
Separates and prioritizes knowledge sources vs conversation memories.
110
"""
111
try:
114
- db = await memory.Memory.get(self.agent)
112
+ db = await Memory.get(self.agent)
113
114
# Search for knowledge sources (knowledge_source=True)
115
knowledge_docs = await db.search_similarity_threshold(
python/tools/search_engine.py
-2
@@ -1,8 +1,6 @@
1
import os
2
import asyncio
3
from python.helpers import dotenv, perplexity_search, duckduckgo_search
4
-from python.helpers.plugins import import_plugin_module
5
-memory = import_plugin_module("memory", "helpers/memory.py")
4
from python.helpers.tool import Tool, Response
5
from python.helpers.print_style import PrintStyle
6
from python.helpers.errors import handle_error
run_ui.py
+15
-7
@@ -468,7 +468,7 @@ def run():
468
runtime.get_arg("host") or dotenv.get_dotenv_value("WEB_UI_HOST") or "localhost"
469
)
470
471
- def register_api_handler(app, handler: type[ApiHandler]):
471
+ def register_api_handler(app, handler: type[ApiHandler], url_prefix: str = ""):
472
name = handler.__module__.split(".")[-1]
473
instance = handler(app, lock)
474
@@ -484,9 +484,10 @@ def run():
484
if handler.requires_csrf():
485
handler_wrap = csrf_protect(handler_wrap)
486
487
+ route = f"{url_prefix}/{name}"
488
app.add_url_rule(
488
- f"/{name}",
489
- f"/{name}",
489
+ route,
490
+ route,
491
handler_wrap,
492
methods=handler.get_methods(),
493
)
@@ -495,12 +496,19 @@ def run():
496
for handler in handlers:
497
register_api_handler(webapp, handler)
498
498
- # Load API handlers from plugins
499
+ # Load API handlers from plugins (prefixed with /plugins/{plugin_id}/)
500
from python.helpers import plugins
500
- plugin_api_paths = plugins.get_plugin_paths("api")
501
- for api_path in plugin_api_paths:
502
- plugin_handlers = load_classes_from_folder(api_path, "*.py", ApiHandler)
501
+
502
+ for plugin in plugins.list_plugins():
503
+ api_path = plugin.path / "api"
504
+ if not api_path.exists() or not api_path.is_dir():
505
+ continue
506
+
507
+ plugin_handlers = load_classes_from_folder(str(api_path), "*.py", ApiHandler)
508
for handler in plugin_handlers:
509
+ # prefixed route for explicit namespacing
510
+ register_api_handler(webapp, handler, url_prefix=f"/plugins/{plugin.id}")
511
+ # bare route so callers don't need to know the plugin prefix
512
register_api_handler(webapp, handler)
513
514
handlers_by_namespace = _build_websocket_handlers_by_namespace(socketio_server, lock)
webui/components/settings/agent/memory.html
+1
-1
@@ -34,7 +34,7 @@
34
<div class="field-control">
35
<button
36
class="btn btn-field"
37
- @click="openModal('../plugins/memory/ui/memory-dashboard.html');"
37
+ @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html');"
38
>
39
Open Dashboard
40
</button>
webui/components/sidebar/top-section/quick-actions.html
+2
-7
@@ -17,7 +17,7 @@
17
</button>
18
19
<!-- Memory -->
20
- <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/ui/memory-dashboard.html')" title="Memory">
20
+ <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html')" title="Memory">
21
<span class="material-symbols-outlined">psychology</span>
22
</button>
23
@@ -53,12 +53,7 @@
53
<span class="material-symbols-outlined">snippet_folder</span>
54
<span>Projects</span>
55
</button>
56
-
57
- <button class="dropdown-item" @click="openModal('../plugins/memory/ui/memory-dashboard.html'); dropdownOpen = false">
58
- <span class="material-symbols-outlined">psychology</span>
59
- <span>Memories</span>
60
- </button>
61
-
56
+
57
<button class="dropdown-item" @click="openModal('modals/scheduler/scheduler-modal.html'); dropdownOpen = false">
58
<span class="material-symbols-outlined">schedule</span>
59
<span>Scheduler</span>
webui/components/welcome/welcome-store.js
+1
-1
@@ -1,7 +1,7 @@
1
import { createStore } from "/js/AlpineStore.js";
2
import { getContext } from "/index.js";
3
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
4
-import { store as memoryStore } from "/plugins/memory/ui/memory-dashboard-store.js";
4
+import { store as memoryStore } from "/plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard-store.js";
5
import { store as projectsStore } from "/components/projects/projects-store.js";
6
import { store as chatInputStore } from "/components/chat/input/input-store.js";
7
import * as API from "/js/api.js";
webui/js/initFw.js
+1
-1
@@ -1,7 +1,7 @@
1
import * as initializer from "./initializer.js";
2
import * as _modals from "./modals.js";
3
import * as _components from "./components.js";
4
-import * as _plugins from "./plugins.js";
4
+import "./plugins.js";
5
import { registerAlpineMagic } from "./confirmClick.js";
6
7
// initialize required elements
webui/js/plugins.js
+44
-212
@@ -1,215 +1,47 @@
1
-// Plugin system for Agent Zero
2
-// Loads x-extension tags by resolving plugin manifests and reusing component infrastructure
3
-
4
-import { importComponent, getParentAttributes } from './components.js';
5
-import { getCsrfToken } from './api.js';
6
-
7
-// Cache for plugin manifests
8
-const extensionCache = {};
9
-
10
-// Batch fetch plugin manifests from API
11
-async function fetchPluginManifests(pluginIds) {
12
- try {
13
- const response = await fetch("/plugins_resolve", {
14
- method: "POST",
15
- headers: {
16
- "Content-Type": "application/json",
17
- "X-CSRF-Token": await getCsrfToken(),
18
- },
19
- body: JSON.stringify({ ids: pluginIds }),
20
- });
21
-
22
- if (!response.ok) {
23
- throw new Error(`Failed to resolve plugins: ${response.statusText}`);
24
- }
25
-
26
- const result = await response.json();
27
- if (!result.ok || !result.data) {
28
- throw new Error("Invalid plugins response");
29
- }
30
-
31
- return result.data;
32
- } catch (error) {
33
- console.error("Error batch-fetching plugin manifests:", error);
34
- return [];
35
- }
36
-}
37
-
38
-// Merge manifest props with element attributes (element attributes override manifest)
39
-function mergePropsWithAttributes(manifestProps, element) {
40
- const elementAttrs = {};
41
- for (let attr of element.attributes) {
42
- if (attr.name !== "id") {
43
- try {
44
- elementAttrs[attr.name] = JSON.parse(attr.value);
45
- } catch (_e) {
46
- elementAttrs[attr.name] = attr.value;
47
- }
48
- }
49
- }
50
- return { ...manifestProps, ...elementAttrs };
51
-}
52
-
53
-// Set merged props as data attributes on element
54
-function setAttributesOnElement(props, element) {
55
- for (const [key, value] of Object.entries(props)) {
56
- if (typeof value === "object") {
57
- element.setAttribute(key, JSON.stringify(value));
58
- } else {
59
- element.setAttribute(key, value);
60
- }
61
- }
62
-}
63
-
64
-// Load a single plugin by calling importComponent with manifest URLs
65
-async function loadPlugin(pluginId, targetElement) {
66
- // Get manifest from cache
67
- let manifest = extensionCache[pluginId];
68
-
69
- if (!manifest || manifest.error) {
70
- throw new Error(manifest?.error || `Plugin '${pluginId}' not found`);
71
- }
72
-
73
- // Extract UI configuration from provides.ui
74
- // The API already resolves component_url and module_url from provides.ui
75
- const componentUrl = manifest.component_url;
76
- const moduleUrl = manifest.module_url;
77
- const props = manifest.props || {};
78
-
79
- // Merge props and set as attributes
80
- const mergedProps = mergePropsWithAttributes(props, targetElement);
81
- setAttributesOnElement(mergedProps, targetElement);
82
-
83
- // Call importComponent with plugin URLs
84
- // We prepend "components/../" to bypass the check in importComponent
85
- if (componentUrl) {
86
- const adjustedUrl = "components/.." + componentUrl;
87
- await importComponent(adjustedUrl, targetElement);
88
- }
89
-
90
- // Load module if specified
91
- // Browser's native module cache handles deduplication
92
- if (moduleUrl) {
93
- await import(moduleUrl);
94
- }
95
-}
96
-
97
-// Find all x-extension tags in root elements
98
-function findAllExtensionTags(roots) {
99
- const rootElements = Array.isArray(roots) ? roots : [roots];
100
- return rootElements.flatMap((root) =>
101
- Array.from(root.querySelectorAll("x-extension"))
102
- );
103
-}
104
-
105
-// Collect unique plugin IDs that need to be fetched (not in cache)
106
-function collectUniqueUncachedPluginIds(extensions) {
107
- const pluginIds = [];
108
- const seen = new Set();
109
-
110
- for (const extension of extensions) {
111
- const pluginId = extension.getAttribute("id");
112
- if (!pluginId) {
113
- console.error("x-extension missing id attribute:", extension);
114
- continue;
115
- }
116
-
117
- // Only add if not seen and not cached
118
- if (!seen.has(pluginId) && !extensionCache[pluginId]) {
119
- pluginIds.push(pluginId);
120
- seen.add(pluginId);
121
- }
122
- }
123
-
124
- return pluginIds;
125
-}
126
-
127
-// Main loader: scan DOM, batch fetch manifests, load all plugins
128
-export async function loadExtensions(roots = [document.documentElement]) {
1
+// Plugin frontend auto-injection.
2
+// Imported once in initFw.js. The backend resolves plugin components and their
3
+// injection targets (parsed from <meta name="plugin-target"> server-side).
4
+// This module simply creates <x-component> elements at the declared targets;
5
+// the standard components.js MutationObserver handles loading automatically.
6
+import { callJsonApi } from "/js/api.js";
7
+
8
+const injected = new Set();
9
+
10
+function tryInject(entry) {
11
+ const key = `${entry.component_url}|${entry.target}`;
12
+ if (injected.has(key)) return true;
13
+ const host = document.querySelector(entry.target);
14
+ if (!host) return false;
15
+ injected.add(key);
16
+ const el = document.createElement("x-component");
17
+ el.setAttribute("path", `../${entry.component_url.replace(/^\/+/, "")}`);
18
+ el.className = "plugin-slot-entry";
19
+ host.appendChild(el);
20
+ return true;
21
+}
22
+
23
+(async () => {
24
+ let res;
25
try {
130
- // Find all x-extension tags
131
- const extensions = findAllExtensionTags(roots);
132
-
133
- if (extensions.length === 0) return;
134
-
135
- // Collect plugin IDs that need fetching
136
- const pluginIds = collectUniqueUncachedPluginIds(extensions);
137
-
138
- // Batch fetch all uncached manifests in one API call
139
- if (pluginIds.length > 0) {
140
- const manifests = await fetchPluginManifests(pluginIds);
141
-
142
- // Update cache with fetched manifests
143
- for (const manifest of manifests) {
144
- if (!manifest.error) {
145
- extensionCache[manifest.id] = manifest;
146
- } else {
147
- console.error(`Plugin '${manifest.id}' failed to load:`, manifest.error);
148
- }
26
+ res = await callJsonApi("/plugins_resolve", {});
27
+ } catch (e) {
28
+ console.warn("Plugin resolve failed:", e);
29
+ return;
30
+ }
31
+ if (!res.ok || !Array.isArray(res.data)) return;
32
+
33
+ // Only auto-inject entries that declare a target; others are standalone (modals etc.)
34
+ const pending = res.data.filter(e => e?.component_url && e?.target);
35
+ const remaining = pending.filter(e => !tryInject(e));
36
+
37
+ // Retry for targets that load after initial render (e.g. sidebar components)
38
+ if (remaining.length > 0) {
39
+ const obs = new MutationObserver(() => {
40
+ for (let i = remaining.length - 1; i >= 0; i--) {
41
+ if (tryInject(remaining[i])) remaining.splice(i, 1);
42
}
150
- }
151
-
152
- // Map plugin IDs to extension elements for parallel loading
153
- const extensionMap = new Map();
154
- for (const extension of extensions) {
155
- const pluginId = extension.getAttribute("id");
156
- if (!pluginId) continue;
157
-
158
- if (!extensionMap.has(pluginId)) {
159
- extensionMap.set(pluginId, []);
160
- }
161
- extensionMap.get(pluginId).push(extension);
162
- }
163
-
164
- // Load all plugins in parallel using cached manifests
165
- await Promise.all(
166
- Array.from(extensionMap.entries()).flatMap(([pluginId, extensionElements]) =>
167
- extensionElements.map(async (extension) => {
168
- try {
169
- await loadPlugin(pluginId, extension);
170
- } catch (error) {
171
- console.error(`Error loading extension '${pluginId}':`, error);
172
- extension.innerHTML = `<div class="error">Failed to load plugin: ${pluginId}</div>`;
173
- }
174
- })
175
- )
176
- );
177
- } catch (error) {
178
- console.error("Error loading extensions:", error);
179
- }
180
-}
181
-
182
-// Extend global xAttrs to check both x-component and x-extension tags
183
-// This allows plugins to use globalThis.xAttrs() and get both component and extension attrs
184
-globalThis.xAttrs = function(el) {
185
- return getParentAttributes(el, ['x-component', 'x-extension']);
186
-};
187
-
188
-// Initialize when DOM is ready
189
-if (document.readyState === 'loading') {
190
- document.addEventListener('DOMContentLoaded', () => loadExtensions());
191
-} else {
192
- loadExtensions();
193
-}
194
-
195
-// Watch for DOM changes to dynamically load x-extension tags
196
-const observer = new MutationObserver((mutations) => {
197
- for (const mutation of mutations) {
198
- for (const node of mutation.addedNodes) {
199
- if (node.nodeType === 1) {
200
- // ELEMENT_NODE
201
- // Check if this node is an x-extension tag
202
- if (node.matches?.("x-extension")) {
203
- loadExtensions([node.parentElement || document.documentElement]);
204
- } else if (node.querySelectorAll) {
205
- // Check if descendants contain x-extension tags
206
- const extensions = node.querySelectorAll("x-extension");
207
- if (extensions.length > 0) {
208
- loadExtensions([node]);
209
- }
210
- }
211
- }
212
- }
43
+ if (remaining.length === 0) obs.disconnect();
44
+ });
45
+ obs.observe(document.body, { childList: true, subtree: true });
46
}
214
-});
215
-observer.observe(document.body, { childList: true, subtree: true });
47
+})();