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 +})();