initial plugins effort; memory system PoC
plugin manifest update add memory plugin PoC files offload memory prompts cleanup imports extract memory UI fix paths plugin docs
3clyp50 committed
Feb 6, 2026 at 18:03 UTC
54fb4746a44003420eff39adf215c65bb0448c93
62 files changed
+1737
-659
.gitignore
+3
-3
@@ -16,9 +16,9 @@
16
.venv/
17
18
# obsolete folders
19
-memory/
20
-knowledge/custom/
21
-instruments/
19
+/memory/
20
+/knowledge/custom/
21
+/instruments/
22
23
# Handle logs directory
24
logs/**
PLUGIN_SYSTEM.md
new
+116
@@ -0,0 +1,116 @@
1
+# Unified Plugin System - Implementation Summary
2
+
3
+This document provides a technical overview of the Plugin System implementation in Agent Zero.
4
+
5
+## Overview
6
+
7
+The Plugin System allows users and the community to create full-stack plugins that integrate seamlessly with Agent Zero. It supports backend capabilities (API endpoints, tools, helpers) and frontend UI extensions. It uses a manifest-driven approach (`plugin.json`) to declare capabilities.
8
+
9
+## Architecture
10
+
11
+### Components
12
+
13
+1. Backend Plugin Discovery (`python/helpers/plugins.py`)
14
+ - Resolves plugin directories across repo (`plugins/`) and user (`usr/plugins/`) locations.
15
+ - Loads and validates `plugin.json` manifests.
16
+ - Provides `import_plugin_module()` for dynamic dependency injection, replacing static imports.
17
+ - Implements override logic (user plugins override repo plugins).
18
+
19
+2. API Endpoints
20
+ - `POST /plugins_resolve` - Resolves a plugin manifest by ID (for frontend).
21
+ - `GET/POST /plugins_list` - Lists all available plugins.
22
+ - `GET /plugins/<id>/<path>` - Serves plugin static assets (UI, scripts).
23
+
24
+3. Frontend Plugin Loader (`webui/js/plugins.js`)
25
+ - Discovers and loads `<x-extension>` tags.
26
+ - Fetches plugin manifests via `/plugins_resolve`.
27
+ - Path Traversal Strategy: Integrates with the standard component loader by adjusting paths (e.g., prepending `components/../` to plugin URLs) to bypass default path restrictions without modifying the core loader.
28
+ - Handles module imports and props merging.
29
+
30
+4. Standard Component Loader (`webui/js/components.js`)
31
+ - Vanilla Implementation: Remains unmodified to ensure stability and compatibility.
32
+ - Enforces `components/` prefix for all loaded resources.
33
+ - Used by `plugins.js` via relative path traversal to load plugin UI components.
34
+
35
+5. DOM Integration
36
+ - Automatic loading on DOM ready.
37
+ - MutationObserver for dynamic plugin injection.
38
+ - Alpine.js integration via `globalThis.xAttrs()`.
39
+
40
+## File Structure
41
+
42
+```
43
+/plugins/ # Repo plugins (default)
44
+ ├── memory/
45
+ │ ├── plugin.json # Manifest
46
+ │ ├── api/ # Backend endpoints
47
+ │ ├── tools/ # Agent tools
48
+ │ ├── helpers/ # Python helpers
49
+ │ ├── ui/ # Frontend components
50
+ │ │ ├── memory-dashboard.html
51
+ │ │ └── memory-dashboard-store.js
52
+ │ └── prompts/ # System prompts
53
+ └── README.md
54
+
55
+/usr/plugins/ # User plugins (override)
56
+ ├── my-plugin/
57
+ │ ├── plugin.json
58
+ │ └── ...
59
+ └── README.md
60
+```
61
+
62
+## Plugin Manifest Schema
63
+
64
+```json
65
+{
66
+ "id": "plugin-id", // Required: Must match directory name
67
+ "name": "Display Name", // Optional: Human-readable name
68
+ "provides": {
69
+ "api": [
70
+ { "module": "api/my_endpoint.py", "description": "..." }
71
+ ],
72
+ "tool": [
73
+ { "module": "tools/my_tool.py", "description": "..." }
74
+ ],
75
+ "ui": {
76
+ "component": "ui/index.html",
77
+ "module": "ui/main.js"
78
+ },
79
+ "prompt": [
80
+ { "module": "prompts/system.md", "description": "..." }
81
+ ]
82
+ },
83
+ "props": { // Optional: Default UI properties
84
+ "key": "value"
85
+ }
86
+}
87
+```
88
+
89
+## Usage
90
+
91
+### Creating a Plugin
92
+
93
+1. Create plugin directory: `plugins/my-plugin`
94
+2. Create `plugin.json` declaring provided capabilities.
95
+3. Implement backend modules in `api/`, `tools/`, etc.
96
+4. Implement frontend components in `ui/`.
97
+
98
+### Using Plugin Capabilities
99
+
100
+Backend (Python):
101
+Instead of static imports, use the dynamic loader:
102
+```python
103
+from python.helpers.plugins import import_plugin_module
104
+
105
+# Dynamically load a helper from the 'memory' plugin
106
+memory = import_plugin_module("memory", "helpers/memory.py")
107
+memory.some_function()
108
+```
109
+
110
+Frontend (HTML/JS):
111
+Plugins can be embedded via `<x-extension>` or loaded dynamically via `openModal`.
112
+```javascript
113
+// Open a plugin component in a modal
114
+// Uses relative path to traverse out of 'components/' and into 'plugins/'
115
+openModal("../plugins/my-plugin/ui/dashboard.html");
116
+```
agent.py
+15
-1
@@ -18,7 +18,8 @@ from python.helpers import (
18
tokens,
19
context as context_helper,
20
dirty_json,
21
- subagents
21
+ subagents,
22
+ plugins
23
)
24
from python.helpers.print_style import PrintStyle
25
@@ -645,6 +646,10 @@ class Agent:
646
647
def parse_prompt(self, _prompt_file: str, **kwargs):
648
dirs = subagents.get_paths(self, "prompts")
649
+
650
+ # Plugin prompt paths
651
+ dirs.extend(plugins.get_plugin_paths("prompt"))
652
+
653
prompt = files.parse_file(
654
_prompt_file, _directories=dirs, _agent=self, **kwargs
655
)
@@ -652,6 +657,10 @@ class Agent:
657
658
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"))
663
+
664
prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs)
665
if files.is_full_json_template(prompt):
666
prompt = files.remove_code_fences(prompt)
@@ -987,6 +996,11 @@ class Agent:
996
997
# 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
+
1004
for path in paths:
1005
try:
1006
classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type]
plugins/README.md
new
+84
@@ -0,0 +1,84 @@
1
+# Agent Zero Plugins
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
+```
49
+
50
+## Capability Types
51
+
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
60
+
61
+## Using Plugins
62
+
63
+### UI Components
64
+```html
65
+<x-extension id="plugin-id"></x-extension>
66
+```
67
+
68
+### Backend Capabilities
69
+Backend capabilities (helpers, tools, extensions, APIs) are automatically discovered and integrated when the plugin is loaded.
70
+
71
+## User Plugins
72
+
73
+User-created plugins should be placed in one of the following directories:
74
+
75
+- `/usr/plugins/` (Global user plugins)
76
+- `/usr/projects/<project_name>/.a0proj/plugins/` (Project-specific plugins)
77
+
78
+Note: `/usr/` refers to the Agent Zero user data directory in the application root, not the system `/usr` directory.
79
+
80
+User plugins with the same ID as repo plugins will completely override the repo version.
81
+
82
+## Documentation
83
+
84
+See [docs/extensibility.md](../docs/extensibility.md) for complete documentation on creating plugins.
plugins/memory/api/memory_dashboard.py
renamed
+8
-1
@@ -1,10 +1,17 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers.memory import Memory, get_existing_memory_subdirs, get_context_memory_subdir
2
from python.helpers import files
3
from models import ModelConfig, ModelType
4
from langchain_core.documents import Document
5
from agent import AgentContext
6
7
+# Import Memory functions from plugin
8
+import sys
9
+from pathlib import Path
10
+_plugin_root = Path(__file__).parent.parent
11
+if str(_plugin_root) not in sys.path:
12
+ sys.path.insert(0, str(_plugin_root))
13
+from helpers.memory import Memory, get_existing_memory_subdirs, get_context_memory_subdir
14
+
15
16
class MemoryDashboard(ApiHandler):
17
plugins/memory/extensions/message_loop_prompts_after/_50_recall_memories.py
new
+222
@@ -0,0 +1,222 @@
1
+import asyncio
2
+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
14
+
15
+
16
+DATA_NAME_TASK = "_recall_memories_task"
17
+DATA_NAME_ITER = "_recall_memories_iter"
18
+
19
+
20
+class RecallMemories(Extension):
21
+
22
+ # INTERVAL = 3
23
+ # HISTORY = 10000
24
+ # MEMORIES_MAX_SEARCH = 12
25
+ # SOLUTIONS_MAX_SEARCH = 8
26
+ # MEMORIES_MAX_RESULT = 5
27
+ # SOLUTIONS_MAX_RESULT = 3
28
+ # THRESHOLD = DEFAULT_MEMORY_THRESHOLD
29
+
30
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
31
+
32
+ set = settings.get_settings()
33
+
34
+ # turned off in settings?
35
+ if not set["memory_recall_enabled"]:
36
+ return
37
+
38
+ # every X iterations (or the first one) recall memories
39
+ if loop_data.iteration % set["memory_recall_interval"] == 0:
40
+
41
+ # show util message right away
42
+ log_item = self.agent.context.log.log(
43
+ type="util",
44
+ heading="Searching memories...",
45
+ )
46
+
47
+ task = asyncio.create_task(
48
+ self.search_memories(loop_data=loop_data, log_item=log_item, **kwargs)
49
+ )
50
+ else:
51
+ task = None
52
+
53
+ # set to agent to be able to wait for it
54
+ self.agent.set_data(DATA_NAME_TASK, task)
55
+ self.agent.set_data(DATA_NAME_ITER, loop_data.iteration)
56
+
57
+ async def search_memories(self, log_item: log.LogItem, loop_data: LoopData, **kwargs):
58
+
59
+ # cleanup
60
+ extras = loop_data.extras_persistent
61
+ if "memories" in extras:
62
+ del extras["memories"]
63
+ if "solutions" in extras:
64
+ del extras["solutions"]
65
+
66
+
67
+ set = settings.get_settings()
68
+ # try:
69
+
70
+ # get system message and chat history for util llm
71
+ system = self.agent.read_prompt("memory.memories_query.sys.md")
72
+
73
+ # # log query streamed by LLM
74
+ # async def log_callback(content):
75
+ # log_item.stream(query=content)
76
+
77
+ # call util llm to summarize conversation
78
+ user_instruction = (
79
+ loop_data.user_message.output_text() if loop_data.user_message else "None"
80
+ )
81
+ history = self.agent.history.output_text()[-set["memory_recall_history_len"]:]
82
+ message = self.agent.read_prompt(
83
+ "memory.memories_query.msg.md", history=history, message=user_instruction
84
+ )
85
+
86
+ # if query preparation by AI is enabled
87
+ if set["memory_recall_query_prep"]:
88
+ try:
89
+ # call util llm to generate search query from the conversation
90
+ query = await self.agent.call_utility_model(
91
+ system=system,
92
+ message=message,
93
+ # callback=log_callback,
94
+ )
95
+ query = query.strip()
96
+ log_item.update(query=query) # no need for streaming here
97
+ except Exception as e:
98
+ err = errors.format_error(e)
99
+ self.agent.context.log.log(
100
+ type="warning", heading="Recall memories extension error:", content=err
101
+ )
102
+ query = ""
103
+
104
+ # no query, no search
105
+ if not query:
106
+ log_item.update(
107
+ heading="Failed to generate memory query",
108
+ )
109
+ return
110
+
111
+ # otherwise use the message and history as query
112
+ else:
113
+ query = user_instruction + "\n\n" + history
114
+
115
+ # if there is no query (or just dash by the LLM), do not continue
116
+ if not query or len(query) <= 3:
117
+ log_item.update(
118
+ query="No relevant memory query generated, skipping search",
119
+ )
120
+ return
121
+
122
+ # get memory database
123
+ db = await Memory.get(self.agent)
124
+
125
+ # search for general memories and fragments
126
+ memories = await db.search_similarity_threshold(
127
+ query=query,
128
+ limit=set["memory_recall_memories_max_search"],
129
+ threshold=set["memory_recall_similarity_threshold"],
130
+ filter=f"area == '{Memory.Area.MAIN.value}' or area == '{Memory.Area.FRAGMENTS.value}'", # exclude solutions
131
+ )
132
+
133
+ # search for solutions
134
+ solutions = await db.search_similarity_threshold(
135
+ query=query,
136
+ limit=set["memory_recall_solutions_max_search"],
137
+ threshold=set["memory_recall_similarity_threshold"],
138
+ filter=f"area == '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
139
+ )
140
+
141
+ if not memories and not solutions:
142
+ log_item.update(
143
+ heading="No memories or solutions found",
144
+ )
145
+ return
146
+
147
+ # if post filtering is enabled
148
+ if set["memory_recall_post_filter"]:
149
+ # assemble an enumerated dict of memories and solutions for AI validation
150
+ mems_list = {i: memory.page_content for i, memory in enumerate(memories + solutions)}
151
+
152
+ # call AI to validate the memories
153
+ try:
154
+ filter = await self.agent.call_utility_model(
155
+ system=self.agent.read_prompt("memory.memories_filter.sys.md"),
156
+ message=self.agent.read_prompt(
157
+ "memory.memories_filter.msg.md",
158
+ memories=mems_list,
159
+ history=history,
160
+ message=user_instruction,
161
+ ),
162
+ )
163
+ filter_inds = dirty_json.try_parse(filter)
164
+
165
+ # filter memories and solutions based on filter_inds
166
+ filtered_memories = []
167
+ filtered_solutions = []
168
+ mem_len = len(memories)
169
+
170
+ # process each index in filter_inds
171
+ # make sure filter_inds is a list and contains valid integers
172
+ if isinstance(filter_inds, list):
173
+ for idx in filter_inds:
174
+ if isinstance(idx, int):
175
+ if idx < mem_len:
176
+ # this is a memory
177
+ filtered_memories.append(memories[idx])
178
+ else:
179
+ # this is a solution, adjust index
180
+ sol_idx = idx - mem_len
181
+ if sol_idx < len(solutions):
182
+ filtered_solutions.append(solutions[sol_idx])
183
+
184
+ # replace original lists with filtered ones
185
+ memories = filtered_memories
186
+ solutions = filtered_solutions
187
+
188
+ except Exception as e:
189
+ err = errors.format_error(e)
190
+ self.agent.context.log.log(
191
+ type="warning", heading="Failed to filter relevant memories", content=err
192
+ )
193
+ filter_inds = []
194
+
195
+
196
+ # limit the number of memories and solutions
197
+ memories = memories[: set["memory_recall_memories_max_result"]]
198
+ solutions = solutions[: set["memory_recall_solutions_max_result"]]
199
+
200
+ # log the search result
201
+ log_item.update(
202
+ heading=f"{len(memories)} memories and {len(solutions)} relevant solutions found",
203
+ )
204
+
205
+ memories_txt = "\n\n".join([mem.page_content for mem in memories]) if memories else ""
206
+ solutions_txt = "\n\n".join([sol.page_content for sol in solutions]) if solutions else ""
207
+
208
+ # log the full results
209
+ if memories_txt:
210
+ log_item.update(memories=memories_txt)
211
+ if solutions_txt:
212
+ log_item.update(solutions=solutions_txt)
213
+
214
+ # place to prompt
215
+ if memories_txt:
216
+ extras["memories"] = self.agent.parse_prompt(
217
+ "agent.system.memories.md", memories=memories_txt
218
+ )
219
+ if solutions_txt:
220
+ extras["solutions"] = self.agent.parse_prompt(
221
+ "agent.system.solutions.md", solutions=solutions_txt
222
+ )
plugins/memory/extensions/monologue_end/_50_memorize_fragments.py
new
+205
@@ -0,0 +1,205 @@
1
+import asyncio
2
+from python.helpers import settings, errors
3
+from python.helpers.extension import Extension
4
+from python.helpers.dirty_json import DirtyJson
5
+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
17
+
18
+
19
+class MemorizeMemories(Extension):
20
+
21
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
22
+ # try:
23
+
24
+ set = settings.get_settings()
25
+
26
+ if not set["memory_memorize_enabled"]:
27
+ return
28
+
29
+ # show full util message
30
+ log_item = self.agent.context.log.log(
31
+ type="util",
32
+ heading="Memorizing new information...",
33
+ )
34
+
35
+ # memorize in background
36
+ task = DeferredTask(thread_name=THREAD_BACKGROUND)
37
+ task.start_task(self.memorize, loop_data, log_item)
38
+ # task = asyncio.create_task(self.memorize(loop_data, log_item))
39
+ return task
40
+
41
+ async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
42
+
43
+ try:
44
+ set = settings.get_settings()
45
+
46
+ db = await Memory.get(self.agent)
47
+
48
+ # get system message and chat history for util llm
49
+ system = self.agent.read_prompt("memory.memories_sum.sys.md")
50
+ msgs_text = self.agent.concat_messages(self.agent.history)
51
+
52
+ # # log query streamed by LLM
53
+ # async def log_callback(content):
54
+ # log_item.stream(content=content)
55
+
56
+ # call util llm to find info in history
57
+ memories_json = await self.agent.call_utility_model(
58
+ system=system,
59
+ message=msgs_text,
60
+ # callback=log_callback,
61
+ background=True,
62
+ )
63
+
64
+ # log data < no need for streaming utility messages
65
+ log_item.update(content=memories_json)
66
+
67
+ # Add validation and error handling for memories_json
68
+ if not memories_json or not isinstance(memories_json, str):
69
+ log_item.update(heading="No response from utility model.")
70
+ return
71
+
72
+ # Strip any whitespace that might cause issues
73
+ memories_json = memories_json.strip()
74
+
75
+ if not memories_json:
76
+ log_item.update(heading="Empty response from utility model.")
77
+ return
78
+
79
+ try:
80
+ memories = DirtyJson.parse_string(memories_json)
81
+ except Exception as e:
82
+ log_item.update(heading=f"Failed to parse memories response: {str(e)}")
83
+ return
84
+
85
+ # Validate that memories is a list or convertible to one
86
+ if memories is None:
87
+ log_item.update(heading="No valid memories found in response.")
88
+ return
89
+
90
+ # If memories is not a list, try to make it one
91
+ if not isinstance(memories, list):
92
+ if isinstance(memories, (str, dict)):
93
+ memories = [memories]
94
+ else:
95
+ log_item.update(heading="Invalid memories format received.")
96
+ return
97
+
98
+ if not isinstance(memories, list) or len(memories) == 0:
99
+ log_item.update(heading="No useful information to memorize.")
100
+ return
101
+ else:
102
+ memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
103
+ log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
104
+
105
+ # Process memories with intelligent consolidation
106
+ total_processed = 0
107
+ total_consolidated = 0
108
+ rem = []
109
+
110
+ for memory in memories:
111
+ # Convert memory to plain text
112
+ txt = f"{memory}"
113
+
114
+ if set["memory_memorize_consolidation"]:
115
+
116
+ try:
117
+ # Use intelligent consolidation system
118
+ from helpers.memory_consolidation import create_memory_consolidator
119
+ consolidator = create_memory_consolidator(
120
+ self.agent,
121
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122
+ max_similar_memories=8,
123
+ max_llm_context_memories=4
124
+ )
125
+
126
+ # Create memory item-specific log for detailed tracking
127
+ memory_log = None # too many utility messages, skip log for now
128
+ # memory_log = self.agent.context.log.log(
129
+ # type="util",
130
+ # heading=f"Processing memory fragment: {txt[:50]}...",
131
+ # update_progress="none" # Don't affect status bar
132
+ # )
133
+
134
+ # Process with intelligent consolidation
135
+ result_obj = await consolidator.process_new_memory(
136
+ new_memory=txt,
137
+ area=Memory.Area.FRAGMENTS.value,
138
+ metadata={"area": Memory.Area.FRAGMENTS.value},
139
+ log_item=memory_log
140
+ )
141
+
142
+ # Update the individual log item with completion status but keep it temporary
143
+ if result_obj.get("success"):
144
+ total_consolidated += 1
145
+ if memory_log:
146
+ memory_log.update(
147
+ result="Fragment processed successfully",
148
+ heading=f"Memory fragment completed: {txt[:50]}...",
149
+ update_progress="none" # Show briefly then disappear
150
+ )
151
+ else:
152
+ if memory_log:
153
+ memory_log.update(
154
+ result="Fragment processing failed",
155
+ heading=f"Memory fragment failed: {txt[:50]}...",
156
+ update_progress="none" # Show briefly then disappear
157
+ )
158
+ total_processed += 1
159
+
160
+ except Exception as e:
161
+ # Log error but continue processing
162
+ log_item.update(consolidation_error=str(e))
163
+ total_processed += 1
164
+
165
+ # Update final results with structured logging
166
+ log_item.update(
167
+ heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
168
+ memories=memories_txt,
169
+ result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
170
+ memories_processed=total_processed,
171
+ memories_consolidated=total_consolidated,
172
+ update_progress="none"
173
+ )
174
+
175
+ else:
176
+
177
+ # remove previous fragments too similiar to this one
178
+ if set["memory_memorize_replace_threshold"] > 0:
179
+ rem += await db.delete_documents_by_query(
180
+ query=txt,
181
+ threshold=set["memory_memorize_replace_threshold"],
182
+ filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
183
+ )
184
+ if rem:
185
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
186
+ log_item.update(replaced=rem_txt)
187
+
188
+ # insert new memory
189
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
190
+
191
+ log_item.update(
192
+ result=f"{len(memories)} entries memorized.",
193
+ heading=f"{len(memories)} entries memorized.",
194
+ )
195
+ if rem:
196
+ log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
197
+
198
+
199
+
200
+
201
+ except Exception as e:
202
+ err = errors.format_error(e)
203
+ self.agent.context.log.log(
204
+ type="warning", heading="Memorize memories extension error", content=err
205
+ )
plugins/memory/extensions/monologue_end/_51_memorize_solutions.py
new
+208
@@ -0,0 +1,208 @@
1
+import asyncio
2
+from python.helpers import settings, errors
3
+from python.helpers.extension import Extension
4
+from python.helpers.dirty_json import DirtyJson
5
+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
17
+
18
+class MemorizeSolutions(Extension):
19
+
20
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
21
+ # try:
22
+
23
+ set = settings.get_settings()
24
+
25
+ if not set["memory_memorize_enabled"]:
26
+ return
27
+
28
+ # show full util message
29
+ log_item = self.agent.context.log.log(
30
+ type="util",
31
+ heading="Memorizing succesful solutions...",
32
+ )
33
+
34
+ # memorize in background
35
+ task = DeferredTask(thread_name=THREAD_BACKGROUND)
36
+ task.start_task(self.memorize, loop_data, log_item)
37
+ # task = asyncio.create_task(self.memorize(loop_data, log_item))
38
+ return task
39
+
40
+ async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
41
+ try:
42
+ set = settings.get_settings()
43
+
44
+ db = await Memory.get(self.agent)
45
+
46
+ # get system message and chat history for util llm
47
+ system = self.agent.read_prompt("memory.solutions_sum.sys.md")
48
+ msgs_text = self.agent.concat_messages(self.agent.history)
49
+
50
+ # log query streamed by LLM
51
+ # async def log_callback(content):
52
+ # log_item.stream(content=content)
53
+
54
+ # call util llm to find solutions in history
55
+ solutions_json = await self.agent.call_utility_model(
56
+ system=system,
57
+ message=msgs_text,
58
+ # callback=log_callback,
59
+ background=True,
60
+ )
61
+
62
+ # log query < no need for streaming utility messages
63
+ log_item.update(content=solutions_json)
64
+
65
+
66
+
67
+ # Add validation and error handling for solutions_json
68
+ if not solutions_json or not isinstance(solutions_json, str):
69
+ log_item.update(heading="No response from utility model.")
70
+ return
71
+
72
+ # Strip any whitespace that might cause issues
73
+ solutions_json = solutions_json.strip()
74
+
75
+ if not solutions_json:
76
+ log_item.update(heading="Empty response from utility model.")
77
+ return
78
+
79
+ try:
80
+ solutions = DirtyJson.parse_string(solutions_json)
81
+ except Exception as e:
82
+ log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
83
+ return
84
+
85
+ # Validate that solutions is a list or convertible to one
86
+ if solutions is None:
87
+ log_item.update(heading="No valid solutions found in response.")
88
+ return
89
+
90
+ # If solutions is not a list, try to make it one
91
+ if not isinstance(solutions, list):
92
+ if isinstance(solutions, (str, dict)):
93
+ solutions = [solutions]
94
+ else:
95
+ log_item.update(heading="Invalid solutions format received.")
96
+ return
97
+
98
+ if not isinstance(solutions, list) or len(solutions) == 0:
99
+ log_item.update(heading="No successful solutions to memorize.")
100
+ return
101
+ else:
102
+ solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
103
+ log_item.update(
104
+ heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
105
+ )
106
+
107
+ # Process solutions with intelligent consolidation
108
+ total_processed = 0
109
+ total_consolidated = 0
110
+ rem = []
111
+
112
+ for solution in solutions:
113
+ # Convert solution to structured text
114
+ if isinstance(solution, dict):
115
+ problem = solution.get('problem', 'Unknown problem')
116
+ solution_text = solution.get('solution', 'Unknown solution')
117
+ txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
118
+ else:
119
+ # If solution is not a dict, convert it to string
120
+ txt = f"# Solution\n {str(solution)}"
121
+
122
+ if set["memory_memorize_consolidation"]:
123
+ try:
124
+ # Use intelligent consolidation system
125
+ from helpers.memory_consolidation import create_memory_consolidator
126
+ consolidator = create_memory_consolidator(
127
+ self.agent,
128
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
129
+ max_similar_memories=6, # Fewer for solutions (more complex)
130
+ max_llm_context_memories=3
131
+ )
132
+
133
+ # Create solution-specific log for detailed tracking
134
+ solution_log = None # too many utility messages, skip log for now
135
+ # solution_log = self.agent.context.log.log(
136
+ # type="util",
137
+ # heading=f"Processing solution: {txt[:50]}...",
138
+ # update_progress="none" # Don't affect status bar
139
+ # )
140
+
141
+ # Process with intelligent consolidation
142
+ result_obj = await consolidator.process_new_memory(
143
+ new_memory=txt,
144
+ area=Memory.Area.SOLUTIONS.value,
145
+ metadata={"area": Memory.Area.SOLUTIONS.value},
146
+ log_item=solution_log
147
+ )
148
+
149
+ # Update the individual log item with completion status but keep it temporary
150
+ if result_obj.get("success"):
151
+ total_consolidated += 1
152
+ if solution_log:
153
+ solution_log.update(
154
+ result="Solution processed successfully",
155
+ heading=f"Solution completed: {txt[:50]}...",
156
+ update_progress="none" # Show briefly then disappear
157
+ )
158
+ else:
159
+ if solution_log:
160
+ solution_log.update(
161
+ result="Solution processing failed",
162
+ heading=f"Solution failed: {txt[:50]}...",
163
+ update_progress="none" # Show briefly then disappear
164
+ )
165
+ total_processed += 1
166
+
167
+ except Exception as e:
168
+ # Log error but continue processing
169
+ log_item.update(consolidation_error=str(e))
170
+ total_processed += 1
171
+
172
+ # Update final results with structured logging
173
+ log_item.update(
174
+ heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
175
+ solutions=solutions_txt,
176
+ result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
177
+ solutions_processed=total_processed,
178
+ solutions_consolidated=total_consolidated,
179
+ update_progress="none"
180
+ )
181
+ else:
182
+ # remove previous solutions too similiar to this one
183
+ if set["memory_memorize_replace_threshold"] > 0:
184
+ rem += await db.delete_documents_by_query(
185
+ query=txt,
186
+ threshold=set["memory_memorize_replace_threshold"],
187
+ filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
188
+ )
189
+ if rem:
190
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
191
+ log_item.update(replaced=rem_txt)
192
+
193
+ # insert new solution
194
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
195
+
196
+ log_item.update(
197
+ result=f"{len(solutions)} solutions memorized.",
198
+ heading=f"{len(solutions)} solutions memorized.",
199
+ )
200
+ if rem:
201
+ log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
202
+
203
+
204
+ except Exception as e:
205
+ err = errors.format_error(e)
206
+ self.agent.context.log.log(
207
+ type="warning", heading="Memorize solutions extension error", content=err
208
+ )
plugins/memory/extensions/monologue_start/_10_memory_init.py
new
+20
@@ -0,0 +1,20 @@
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/helpers/knowledge_import.py
renamed
plugins/memory/helpers/memory.py
renamed
+2
-2
@@ -23,9 +23,9 @@ import os, json
23
import numpy as np
24
25
from python.helpers.print_style import PrintStyle
26
-from . import files
26
+from python.helpers import files
27
from langchain_core.documents import Document
28
-from python.helpers import knowledge_import
28
+from . import knowledge_import
29
from python.helpers.log import Log, LogItem
30
from enum import Enum
31
from agent import Agent, AgentContext
plugins/memory/helpers/memory_consolidation.py
renamed
+9
-2
@@ -7,13 +7,20 @@ from enum import Enum
7
8
from langchain_core.documents import Document
9
10
-from python.helpers.memory import Memory
10
+from .memory import Memory
11
from python.helpers.dirty_json import DirtyJson
12
from python.helpers.log import LogItem
13
from python.helpers.print_style import PrintStyle
14
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
14
from agent import Agent
15
16
+# Import from tools within plugin
17
+import sys
18
+from pathlib import Path
19
+_plugin_root = Path(__file__).parent.parent
20
+if str(_plugin_root) not in sys.path:
21
+ sys.path.insert(0, str(_plugin_root))
22
+from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
23
+
24
25
class ConsolidationAction(Enum):
26
"""Actions that can be taken during memory consolidation."""
plugins/memory/plugin.json
new
+151
@@ -0,0 +1,151 @@
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/prompts/agent.system.memories.md
renamed
plugins/memory/prompts/agent.system.solutions.md
renamed
plugins/memory/prompts/agent.system.tool.memory.md
renamed
plugins/memory/prompts/fw.memory.hist_suc.sys.md
renamed
plugins/memory/prompts/fw.memory.hist_sum.sys.md
renamed
plugins/memory/prompts/fw.memory_saved.md
renamed
plugins/memory/prompts/memory.consolidation.msg.md
renamed
plugins/memory/prompts/memory.consolidation.sys.md
renamed
plugins/memory/prompts/memory.keyword_extraction.msg.md
renamed
plugins/memory/prompts/memory.keyword_extraction.sys.md
renamed
plugins/memory/prompts/memory.memories_filter.msg.md
renamed
plugins/memory/prompts/memory.memories_filter.sys.md
renamed
plugins/memory/prompts/memory.memories_query.msg.md
renamed
plugins/memory/prompts/memory.memories_query.sys.md
renamed
plugins/memory/prompts/memory.memories_sum.sys.md
renamed
plugins/memory/prompts/memory.recall_delay_msg.md
renamed
plugins/memory/prompts/memory.solutions_query.sys.md
renamed
plugins/memory/prompts/memory.solutions_sum.sys.md
renamed
plugins/memory/tools/memory_delete.py
renamed
+8
-1
@@ -1,6 +1,13 @@
1
-from python.helpers.memory import Memory
1
from python.helpers.tool import Tool, Response
2
3
+# Import Memory from plugin
4
+import sys
5
+from pathlib import Path
6
+_plugin_root = Path(__file__).parent.parent
7
+if str(_plugin_root) not in sys.path:
8
+ sys.path.insert(0, str(_plugin_root))
9
+from helpers.memory import Memory
10
+
11
12
class MemoryDelete(Tool):
13
plugins/memory/tools/memory_forget.py
renamed
+9
-2
@@ -1,6 +1,13 @@
1
-from python.helpers.memory import Memory
1
from python.helpers.tool import Tool, Response
3
-from python.tools.memory_load import DEFAULT_THRESHOLD
2
+
3
+# Import Memory and DEFAULT_THRESHOLD from plugin
4
+import sys
5
+from pathlib import Path
6
+_plugin_root = Path(__file__).parent.parent
7
+if str(_plugin_root) not in sys.path:
8
+ sys.path.insert(0, str(_plugin_root))
9
+from helpers.memory import Memory
10
+from tools.memory_load import DEFAULT_THRESHOLD
11
12
13
class MemoryForget(Tool):
plugins/memory/tools/memory_load.py
renamed
+8
-1
@@ -1,6 +1,13 @@
1
-from python.helpers.memory import Memory
1
from python.helpers.tool import Tool, Response
2
3
+# Import Memory from plugin
4
+import sys
5
+from pathlib import Path
6
+_plugin_root = Path(__file__).parent.parent
7
+if str(_plugin_root) not in sys.path:
8
+ sys.path.insert(0, str(_plugin_root))
9
+from helpers.memory import Memory
10
+
11
DEFAULT_THRESHOLD = 0.7
12
DEFAULT_LIMIT = 10
13
plugins/memory/tools/memory_save.py
renamed
+8
-1
@@ -1,6 +1,13 @@
1
-from python.helpers.memory import Memory
1
from python.helpers.tool import Tool, Response
2
3
+# Import Memory from plugin
4
+import sys
5
+from pathlib import Path
6
+_plugin_root = Path(__file__).parent.parent
7
+if str(_plugin_root) not in sys.path:
8
+ sys.path.insert(0, str(_plugin_root))
9
+from helpers.memory import Memory
10
+
11
12
class MemorySave(Tool):
13
plugins/memory/ui/memory-dashboard-store.js
renamed
+2
-2
@@ -53,7 +53,7 @@ const memoryDashboardStore = {
53
pollingEnabled: false,
54
55
async openModal() {
56
- await openModal("modals/memory/memory-dashboard.html");
56
+ await openModal("../plugins/memory/ui/memory-dashboard.html");
57
},
58
59
init() {
@@ -448,7 +448,7 @@ ${memory.content_full}
448
this.editMode = false;
449
this.editMemoryBackup = null;
450
// Use global modal system
451
- openModal("modals/memory/memory-detail-modal.html");
451
+ openModal("../plugins/memory/ui/memory-detail-modal.html");
452
},
453
454
closeMemoryDetails() {
plugins/memory/ui/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 "/components/modals/memory/memory-dashboard-store.js";
6
+ import { store } from "/plugins/memory/ui/memory-dashboard-store.js";
7
</script>
8
</head>
9
plugins/memory/ui/memory-detail-modal.html
renamed
python/api/import_knowledge.py
+3
-1
@@ -1,5 +1,7 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, memory
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
6
from python.helpers.security import safe_filename
7
python/api/knowledge_path_get.py
+3
-1
@@ -1,5 +1,7 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, memory, notification, projects, notification
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
7
python/api/knowledge_reindex.py
+3
-1
@@ -1,5 +1,7 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import files, memory, notification, projects, notification
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
7
python/api/plugins_list.py
new
+22
@@ -0,0 +1,22 @@
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
new
+50
@@ -0,0 +1,50 @@
1
+from python.helpers.api import ApiHandler, Request, Response
2
+from python.helpers import plugins
3
+
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": [{...}, {...}]}
12
+ """
13
+
14
+ @classmethod
15
+ def get_methods(cls):
16
+ return ["POST"]
17
+
18
+ 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"}
python/extensions/message_loop_prompts_after/_50_recall_memories.py
+6
-217
@@ -1,219 +1,8 @@
1
-import asyncio
2
-from python.helpers.extension import Extension
3
-from python.helpers.memory import Memory
4
-from agent import LoopData
5
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
6
-from python.helpers import dirty_json, errors, settings, log
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
9
-DATA_NAME_TASK = "_recall_memories_task"
10
-DATA_NAME_ITER = "_recall_memories_iter"
11
-SEARCH_TIMEOUT = 30
12
-
13
-
14
-class RecallMemories(Extension):
15
-
16
- # INTERVAL = 3
17
- # HISTORY = 10000
18
- # MEMORIES_MAX_SEARCH = 12
19
- # SOLUTIONS_MAX_SEARCH = 8
20
- # MEMORIES_MAX_RESULT = 5
21
- # SOLUTIONS_MAX_RESULT = 3
22
- # THRESHOLD = DEFAULT_MEMORY_THRESHOLD
23
-
24
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
25
-
26
- set = settings.get_settings()
27
-
28
- # turned off in settings?
29
- if not set["memory_recall_enabled"]:
30
- return
31
-
32
- # every X iterations (or the first one) recall memories
33
- if loop_data.iteration % set["memory_recall_interval"] == 0:
34
-
35
- # show util message right away
36
- log_item = self.agent.context.log.log(
37
- type="util",
38
- heading="Searching memories...",
39
- )
40
-
41
- task = asyncio.create_task(
42
- asyncio.wait_for(
43
- self.search_memories(loop_data=loop_data, log_item=log_item, **kwargs),
44
- timeout=SEARCH_TIMEOUT,
45
- )
46
- )
47
- else:
48
- task = None
49
-
50
- # set to agent to be able to wait for it
51
- self.agent.set_data(DATA_NAME_TASK, task)
52
- self.agent.set_data(DATA_NAME_ITER, loop_data.iteration)
53
-
54
- async def search_memories(self, log_item: log.LogItem, loop_data: LoopData, **kwargs):
55
-
56
- # cleanup
57
- extras = loop_data.extras_persistent
58
- if "memories" in extras:
59
- del extras["memories"]
60
- if "solutions" in extras:
61
- del extras["solutions"]
62
-
63
-
64
- set = settings.get_settings()
65
- # try:
66
-
67
- # get system message and chat history for util llm
68
- system = self.agent.read_prompt("memory.memories_query.sys.md")
69
-
70
- # # log query streamed by LLM
71
- # async def log_callback(content):
72
- # log_item.stream(query=content)
73
-
74
- # call util llm to summarize conversation
75
- user_instruction = (
76
- loop_data.user_message.output_text() if loop_data.user_message else "None"
77
- )
78
- history = self.agent.history.output_text()[-set["memory_recall_history_len"]:]
79
- message = self.agent.read_prompt(
80
- "memory.memories_query.msg.md", history=history, message=user_instruction
81
- )
82
-
83
- # if query preparation by AI is enabled
84
- if set["memory_recall_query_prep"]:
85
- try:
86
- # call util llm to generate search query from the conversation
87
- query = await self.agent.call_utility_model(
88
- system=system,
89
- message=message,
90
- # callback=log_callback,
91
- )
92
- query = query.strip()
93
- log_item.update(query=query) # no need for streaming here
94
- except Exception as e:
95
- err = errors.format_error(e)
96
- self.agent.context.log.log(
97
- type="warning", heading="Recall memories extension error:", content=err
98
- )
99
- query = ""
100
-
101
- # no query, no search
102
- if not query:
103
- log_item.update(
104
- heading="Failed to generate memory query",
105
- )
106
- return
107
-
108
- # otherwise use the message and history as query
109
- else:
110
- query = user_instruction + "\n\n" + history
111
-
112
- # if there is no query (or just dash by the LLM), do not continue
113
- if not query or len(query) <= 3:
114
- log_item.update(
115
- query="No relevant memory query generated, skipping search",
116
- )
117
- return
118
-
119
- # get memory database
120
- db = await Memory.get(self.agent)
121
-
122
- # search for general memories and fragments
123
- memories = await db.search_similarity_threshold(
124
- query=query,
125
- limit=set["memory_recall_memories_max_search"],
126
- threshold=set["memory_recall_similarity_threshold"],
127
- filter=f"area == '{Memory.Area.MAIN.value}' or area == '{Memory.Area.FRAGMENTS.value}'", # exclude solutions
128
- )
129
-
130
- # search for solutions
131
- solutions = await db.search_similarity_threshold(
132
- query=query,
133
- limit=set["memory_recall_solutions_max_search"],
134
- threshold=set["memory_recall_similarity_threshold"],
135
- filter=f"area == '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
136
- )
137
-
138
- if not memories and not solutions:
139
- log_item.update(
140
- heading="No memories or solutions found",
141
- )
142
- return
143
-
144
- # if post filtering is enabled
145
- if set["memory_recall_post_filter"]:
146
- # assemble an enumerated dict of memories and solutions for AI validation
147
- mems_list = {i: memory.page_content for i, memory in enumerate(memories + solutions)}
148
-
149
- # call AI to validate the memories
150
- try:
151
- filter = await self.agent.call_utility_model(
152
- system=self.agent.read_prompt("memory.memories_filter.sys.md"),
153
- message=self.agent.read_prompt(
154
- "memory.memories_filter.msg.md",
155
- memories=mems_list,
156
- history=history,
157
- message=user_instruction,
158
- ),
159
- )
160
- filter_inds = dirty_json.try_parse(filter)
161
-
162
- # filter memories and solutions based on filter_inds
163
- filtered_memories = []
164
- filtered_solutions = []
165
- mem_len = len(memories)
166
-
167
- # process each index in filter_inds
168
- # make sure filter_inds is a list and contains valid integers
169
- if isinstance(filter_inds, list):
170
- for idx in filter_inds:
171
- if isinstance(idx, int):
172
- if idx < mem_len:
173
- # this is a memory
174
- filtered_memories.append(memories[idx])
175
- else:
176
- # this is a solution, adjust index
177
- sol_idx = idx - mem_len
178
- if sol_idx < len(solutions):
179
- filtered_solutions.append(solutions[sol_idx])
180
-
181
- # replace original lists with filtered ones
182
- memories = filtered_memories
183
- solutions = filtered_solutions
184
-
185
- except Exception as e:
186
- err = errors.format_error(e)
187
- self.agent.context.log.log(
188
- type="warning", heading="Failed to filter relevant memories", content=err
189
- )
190
- filter_inds = []
191
-
192
-
193
- # limit the number of memories and solutions
194
- memories = memories[: set["memory_recall_memories_max_result"]]
195
- solutions = solutions[: set["memory_recall_solutions_max_result"]]
196
-
197
- # log the search result
198
- log_item.update(
199
- heading=f"{len(memories)} memories and {len(solutions)} relevant solutions found",
200
- )
201
-
202
- memories_txt = "\n\n".join([mem.page_content for mem in memories]) if memories else ""
203
- solutions_txt = "\n\n".join([sol.page_content for sol in solutions]) if solutions else ""
204
-
205
- # log the full results
206
- if memories_txt:
207
- log_item.update(memories=memories_txt)
208
- if solutions_txt:
209
- log_item.update(solutions=solutions_txt)
210
-
211
- # place to prompt
212
- if memories_txt:
213
- extras["memories"] = self.agent.parse_prompt(
214
- "agent.system.memories.md", memories=memories_txt
215
- )
216
- if solutions_txt:
217
- extras["solutions"] = self.agent.parse_prompt(
218
- "agent.system.solutions.md", solutions=solutions_txt
219
- )
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
+6
-196
@@ -1,198 +1,8 @@
1
-import asyncio
2
-from python.helpers import settings, errors
3
-from python.helpers.extension import Extension
4
-from python.helpers.memory import Memory
5
-from python.helpers.dirty_json import DirtyJson
6
-from agent import LoopData
7
-from python.helpers.log import LogItem
8
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
-from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
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
12
-class MemorizeMemories(Extension):
13
-
14
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
15
- # try:
16
-
17
- set = settings.get_settings()
18
-
19
- if not set["memory_memorize_enabled"]:
20
- return
21
-
22
- # show full util message
23
- log_item = self.agent.context.log.log(
24
- type="util",
25
- heading="Memorizing new information...",
26
- )
27
-
28
- # memorize in background
29
- task = DeferredTask(thread_name=THREAD_BACKGROUND)
30
- task.start_task(self.memorize, loop_data, log_item)
31
- # task = asyncio.create_task(self.memorize(loop_data, log_item))
32
- return task
33
-
34
- async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
35
-
36
- try:
37
- set = settings.get_settings()
38
-
39
- db = await Memory.get(self.agent)
40
-
41
- # get system message and chat history for util llm
42
- system = self.agent.read_prompt("memory.memories_sum.sys.md")
43
- msgs_text = self.agent.concat_messages(self.agent.history)
44
-
45
- # # log query streamed by LLM
46
- # async def log_callback(content):
47
- # log_item.stream(content=content)
48
-
49
- # call util llm to find info in history
50
- memories_json = await self.agent.call_utility_model(
51
- system=system,
52
- message=msgs_text,
53
- # callback=log_callback,
54
- background=True,
55
- )
56
-
57
- # log data < no need for streaming utility messages
58
- log_item.update(content=memories_json)
59
-
60
- # Add validation and error handling for memories_json
61
- if not memories_json or not isinstance(memories_json, str):
62
- log_item.update(heading="No response from utility model.")
63
- return
64
-
65
- # Strip any whitespace that might cause issues
66
- memories_json = memories_json.strip()
67
-
68
- if not memories_json:
69
- log_item.update(heading="Empty response from utility model.")
70
- return
71
-
72
- try:
73
- memories = DirtyJson.parse_string(memories_json)
74
- except Exception as e:
75
- log_item.update(heading=f"Failed to parse memories response: {str(e)}")
76
- return
77
-
78
- # Validate that memories is a list or convertible to one
79
- if memories is None:
80
- log_item.update(heading="No valid memories found in response.")
81
- return
82
-
83
- # If memories is not a list, try to make it one
84
- if not isinstance(memories, list):
85
- if isinstance(memories, (str, dict)):
86
- memories = [memories]
87
- else:
88
- log_item.update(heading="Invalid memories format received.")
89
- return
90
-
91
- if not isinstance(memories, list) or len(memories) == 0:
92
- log_item.update(heading="No useful information to memorize.")
93
- return
94
- else:
95
- memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
96
- log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
97
-
98
- # Process memories with intelligent consolidation
99
- total_processed = 0
100
- total_consolidated = 0
101
- rem = []
102
-
103
- for memory in memories:
104
- # Convert memory to plain text
105
- txt = f"{memory}"
106
-
107
- if set["memory_memorize_consolidation"]:
108
-
109
- try:
110
- # Use intelligent consolidation system
111
- from python.helpers.memory_consolidation import create_memory_consolidator
112
- consolidator = create_memory_consolidator(
113
- self.agent,
114
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
115
- max_similar_memories=8,
116
- max_llm_context_memories=4
117
- )
118
-
119
- # Create memory item-specific log for detailed tracking
120
- memory_log = None # too many utility messages, skip log for now
121
- # memory_log = self.agent.context.log.log(
122
- # type="util",
123
- # heading=f"Processing memory fragment: {txt[:50]}...",
124
- # update_progress="none" # Don't affect status bar
125
- # )
126
-
127
- # Process with intelligent consolidation
128
- result_obj = await consolidator.process_new_memory(
129
- new_memory=txt,
130
- area=Memory.Area.FRAGMENTS.value,
131
- metadata={"area": Memory.Area.FRAGMENTS.value},
132
- log_item=memory_log
133
- )
134
-
135
- # Update the individual log item with completion status but keep it temporary
136
- if result_obj.get("success"):
137
- total_consolidated += 1
138
- if memory_log:
139
- memory_log.update(
140
- result="Fragment processed successfully",
141
- heading=f"Memory fragment completed: {txt[:50]}...",
142
- update_progress="none" # Show briefly then disappear
143
- )
144
- else:
145
- if memory_log:
146
- memory_log.update(
147
- result="Fragment processing failed",
148
- heading=f"Memory fragment failed: {txt[:50]}...",
149
- update_progress="none" # Show briefly then disappear
150
- )
151
- total_processed += 1
152
-
153
- except Exception as e:
154
- # Log error but continue processing
155
- log_item.update(consolidation_error=str(e))
156
- total_processed += 1
157
-
158
- # Update final results with structured logging
159
- log_item.update(
160
- heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
161
- memories=memories_txt,
162
- result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
163
- memories_processed=total_processed,
164
- memories_consolidated=total_consolidated,
165
- update_progress="none"
166
- )
167
-
168
- else:
169
-
170
- # remove previous fragments too similiar to this one
171
- if set["memory_memorize_replace_threshold"] > 0:
172
- rem += await db.delete_documents_by_query(
173
- query=txt,
174
- threshold=set["memory_memorize_replace_threshold"],
175
- filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
176
- )
177
- if rem:
178
- rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
179
- log_item.update(replaced=rem_txt)
180
-
181
- # insert new memory
182
- await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
183
-
184
- log_item.update(
185
- result=f"{len(memories)} entries memorized.",
186
- heading=f"{len(memories)} entries memorized.",
187
- )
188
- if rem:
189
- log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
190
-
191
-
192
-
193
-
194
- except Exception as e:
195
- err = errors.format_error(e)
196
- self.agent.context.log.log(
197
- type="warning", heading="Memorize memories extension error", content=err
198
- )
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
+6
-199
@@ -1,201 +1,8 @@
1
-import asyncio
2
-from python.helpers import settings, errors
3
-from python.helpers.extension import Extension
4
-from python.helpers.memory import Memory
5
-from python.helpers.dirty_json import DirtyJson
6
-from agent import LoopData
7
-from python.helpers.log import LogItem
8
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
-from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
1
+"""Memorize solutions extension - implementation provided by the memory plugin."""
2
+from python.helpers.plugins import import_plugin_module
3
11
-class MemorizeSolutions(Extension):
4
+# Import the actual implementation from the plugin
5
+_mod = import_plugin_module("memory", "extensions/monologue_end/_51_memorize_solutions.py")
6
13
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14
- # try:
15
-
16
- set = settings.get_settings()
17
-
18
- if not set["memory_memorize_enabled"]:
19
- return
20
-
21
- # show full util message
22
- log_item = self.agent.context.log.log(
23
- type="util",
24
- heading="Memorizing succesful solutions...",
25
- )
26
-
27
- # memorize in background
28
- task = DeferredTask(thread_name=THREAD_BACKGROUND)
29
- task.start_task(self.memorize, loop_data, log_item)
30
- # task = asyncio.create_task(self.memorize(loop_data, log_item))
31
- return task
32
-
33
- async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
34
- try:
35
- set = settings.get_settings()
36
-
37
- db = await Memory.get(self.agent)
38
-
39
- # get system message and chat history for util llm
40
- system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41
- msgs_text = self.agent.concat_messages(self.agent.history)
42
-
43
- # log query streamed by LLM
44
- # async def log_callback(content):
45
- # log_item.stream(content=content)
46
-
47
- # call util llm to find solutions in history
48
- solutions_json = await self.agent.call_utility_model(
49
- system=system,
50
- message=msgs_text,
51
- # callback=log_callback,
52
- background=True,
53
- )
54
-
55
- # log query < no need for streaming utility messages
56
- log_item.update(content=solutions_json)
57
-
58
-
59
-
60
- # Add validation and error handling for solutions_json
61
- if not solutions_json or not isinstance(solutions_json, str):
62
- log_item.update(heading="No response from utility model.")
63
- return
64
-
65
- # Strip any whitespace that might cause issues
66
- solutions_json = solutions_json.strip()
67
-
68
- if not solutions_json:
69
- log_item.update(heading="Empty response from utility model.")
70
- return
71
-
72
- try:
73
- solutions = DirtyJson.parse_string(solutions_json)
74
- except Exception as e:
75
- log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76
- return
77
-
78
- # Validate that solutions is a list or convertible to one
79
- if solutions is None:
80
- log_item.update(heading="No valid solutions found in response.")
81
- return
82
-
83
- # If solutions is not a list, try to make it one
84
- if not isinstance(solutions, list):
85
- if isinstance(solutions, (str, dict)):
86
- solutions = [solutions]
87
- else:
88
- log_item.update(heading="Invalid solutions format received.")
89
- return
90
-
91
- if not isinstance(solutions, list) or len(solutions) == 0:
92
- log_item.update(heading="No successful solutions to memorize.")
93
- return
94
- else:
95
- solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96
- log_item.update(
97
- heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98
- )
99
-
100
- # Process solutions with intelligent consolidation
101
- total_processed = 0
102
- total_consolidated = 0
103
- rem = []
104
-
105
- for solution in solutions:
106
- # Convert solution to structured text
107
- if isinstance(solution, dict):
108
- problem = solution.get('problem', 'Unknown problem')
109
- solution_text = solution.get('solution', 'Unknown solution')
110
- txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
111
- else:
112
- # If solution is not a dict, convert it to string
113
- txt = f"# Solution\n {str(solution)}"
114
-
115
- if set["memory_memorize_consolidation"]:
116
- try:
117
- # Use intelligent consolidation system
118
- from python.helpers.memory_consolidation import create_memory_consolidator
119
- consolidator = create_memory_consolidator(
120
- self.agent,
121
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122
- max_similar_memories=6, # Fewer for solutions (more complex)
123
- max_llm_context_memories=3
124
- )
125
-
126
- # Create solution-specific log for detailed tracking
127
- solution_log = None # too many utility messages, skip log for now
128
- # solution_log = self.agent.context.log.log(
129
- # type="util",
130
- # heading=f"Processing solution: {txt[:50]}...",
131
- # update_progress="none" # Don't affect status bar
132
- # )
133
-
134
- # Process with intelligent consolidation
135
- result_obj = await consolidator.process_new_memory(
136
- new_memory=txt,
137
- area=Memory.Area.SOLUTIONS.value,
138
- metadata={"area": Memory.Area.SOLUTIONS.value},
139
- log_item=solution_log
140
- )
141
-
142
- # Update the individual log item with completion status but keep it temporary
143
- if result_obj.get("success"):
144
- total_consolidated += 1
145
- if solution_log:
146
- solution_log.update(
147
- result="Solution processed successfully",
148
- heading=f"Solution completed: {txt[:50]}...",
149
- update_progress="none" # Show briefly then disappear
150
- )
151
- else:
152
- if solution_log:
153
- solution_log.update(
154
- result="Solution processing failed",
155
- heading=f"Solution failed: {txt[:50]}...",
156
- update_progress="none" # Show briefly then disappear
157
- )
158
- total_processed += 1
159
-
160
- except Exception as e:
161
- # Log error but continue processing
162
- log_item.update(consolidation_error=str(e))
163
- total_processed += 1
164
-
165
- # Update final results with structured logging
166
- log_item.update(
167
- heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168
- solutions=solutions_txt,
169
- result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170
- solutions_processed=total_processed,
171
- solutions_consolidated=total_consolidated,
172
- update_progress="none"
173
- )
174
- else:
175
- # remove previous solutions too similiar to this one
176
- if set["memory_memorize_replace_threshold"] > 0:
177
- rem += await db.delete_documents_by_query(
178
- query=txt,
179
- threshold=set["memory_memorize_replace_threshold"],
180
- filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
181
- )
182
- if rem:
183
- rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184
- log_item.update(replaced=rem_txt)
185
-
186
- # insert new solution
187
- await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188
-
189
- log_item.update(
190
- result=f"{len(solutions)} solutions memorized.",
191
- heading=f"{len(solutions)} solutions memorized.",
192
- )
193
- if rem:
194
- log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
195
-
196
-
197
- except Exception as e:
198
- err = errors.format_error(e)
199
- self.agent.context.log.log(
200
- type="warning", heading="Memorize solutions extension error", content=err
201
- )
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
+6
-11
@@ -1,13 +1,8 @@
1
-from python.helpers.extension import Extension
2
-from agent import LoopData
3
-from python.helpers import memory
4
-import asyncio
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
-class MemoryInit(Extension):
8
-
9
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
10
- db = await memory.Memory.get(self.agent)
11
-
12
-
13
-
\ No newline at end of file
7
+# Re-export all public names
8
+globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/system_prompt/_20_behaviour_prompt.py
+4
-1
@@ -1,7 +1,10 @@
1
from datetime import datetime
2
from python.helpers.extension import Extension
3
from agent import Agent, LoopData
4
-from python.helpers import files, memory
4
+from python.helpers import files
5
+from python.helpers.plugins import import_plugin_module
6
+
7
+memory = import_plugin_module("memory", "helpers/memory.py")
8
9
10
class BehaviourPrompt(Extension):
python/helpers/extension.py
+6
-1
@@ -27,10 +27,15 @@ class Extension:
27
async def call_extensions(
28
extension_point: str, agent: "Agent|None" = None, **kwargs
29
) -> Any:
30
- from python.helpers import projects, subagents
30
+ from python.helpers import projects, subagents, plugins
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
+
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
new
+262
@@ -0,0 +1,262 @@
1
+from __future__ import annotations
2
+
3
+import json
4
+from dataclasses import dataclass, field
5
+from pathlib import Path
6
+from typing import Any, Dict, List, Optional
7
+
8
+from python.helpers import files
9
+
10
+
11
+# ============================================================================
12
+# Core Data Structures
13
+# ============================================================================
14
+
15
+@dataclass(slots=True)
16
+class Plugin:
17
+ id: str
18
+ name: str
19
+ 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)
29
+
30
+
31
+# ============================================================================
32
+# Discovery & Loading
33
+# ============================================================================
34
+
35
+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
+
43
+ return [
44
+ *projects,
45
+ files.get_abs_path("usr/plugins"),
46
+ files.get_abs_path("plugins"),
47
+ ]
48
+
49
+
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
+
101
+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)
112
+ 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
+
117
+ return list(by_id.values())
118
+
119
+
120
+def find_plugin(plugin_id: str) -> Optional[Plugin]:
121
+ """Find a specific plugin by ID."""
122
+ if not plugin_id:
123
+ 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
+
132
+ return None
133
+
134
+
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]:
145
+ """
146
+ Get all paths from loaded plugins for a given capability type.
147
+
148
+ Args:
149
+ cap_type: Capability type (e.g., "tool", "extension", "api")
150
+ subpaths: Additional path components to append
151
+
152
+ Returns:
153
+ List of absolute paths matching the capability type
154
+ """
155
+ paths: List[str] = []
156
+
157
+ for plugin in list_plugins():
158
+ cap_config = plugin.provides.get(cap_type)
159
+ if not cap_config:
160
+ continue
161
+
162
+ # Normalize to list for uniform processing
163
+ modules = []
164
+ if isinstance(cap_config, list):
165
+ modules = [cap.get("module") for cap in cap_config if isinstance(cap, dict) and cap.get("module")]
166
+ elif isinstance(cap_config, dict) and cap_config.get("module"):
167
+ modules = [cap_config["module"]]
168
+
169
+ # Build paths from modules
170
+ for module_path in modules:
171
+ module_dir = _extract_module_dir(module_path)
172
+ full_path = files.get_abs_path(str(plugin.path), module_dir, *subpaths)
173
+ if files.exists(full_path) and full_path not in paths:
174
+ paths.append(full_path)
175
+
176
+ return paths
177
+
178
+
179
+def import_plugin_module(plugin_id: str, module_path: str) -> Any:
180
+ """
181
+ Import a Python module from a plugin using importlib.
182
+
183
+ Args:
184
+ plugin_id: Plugin ID
185
+ module_path: Relative path to module within plugin (e.g., "helpers/memory.py")
186
+
187
+ Returns:
188
+ The imported module object
189
+
190
+ Raises:
191
+ ImportError: If plugin or module not found
192
+ """
193
+ import importlib.util
194
+ import sys
195
+
196
+ plugin = find_plugin(plugin_id)
197
+ if not plugin:
198
+ raise ImportError(f"Plugin '{plugin_id}' not found")
199
+
200
+ full_path = plugin.path / module_path
201
+ if not full_path.exists():
202
+ raise ImportError(f"Module '{module_path}' not found in plugin '{plugin_id}'")
203
+
204
+ # Create a unique module name to avoid conflicts
205
+ module_name = f"plugins.{plugin_id}.{module_path.replace('/', '.').replace('.py', '')}"
206
+
207
+ # Check if already loaded
208
+ if module_name in sys.modules:
209
+ return sys.modules[module_name]
210
+
211
+ # Load the module
212
+ spec = importlib.util.spec_from_file_location(module_name, full_path)
213
+ if spec is None or spec.loader is None:
214
+ raise ImportError(f"Could not load module spec from {full_path}")
215
+
216
+ module = importlib.util.module_from_spec(spec)
217
+ sys.modules[module_name] = module
218
+ spec.loader.exec_module(module)
219
+
220
+ return module
221
+
222
+
223
+# ============================================================================
224
+# API Helpers
225
+# ============================================================================
226
+
227
+def build_plugin_response_data(plugin: Plugin) -> dict:
228
+ """
229
+ Build normalized API response data for a plugin.
230
+ Resolves URLs for UI capabilities.
231
+
232
+ Args:
233
+ plugin: Plugin object to serialize
234
+
235
+ Returns:
236
+ Dictionary with plugin data, component_url, module_url, and provides
237
+ """
238
+ base_url = f"/plugins/{plugin.id}/"
239
+
240
+ response_data = {
241
+ "id": plugin.id,
242
+ "name": plugin.name,
243
+ "base_url": base_url,
244
+ "version": plugin.version,
245
+ "author": plugin.author,
246
+ "description": plugin.description,
247
+ "tags": plugin.tags,
248
+ "provides": plugin.provides,
249
+ }
250
+
251
+ # Resolve UI capability URLs from provides.ui
252
+ if "ui" in plugin.provides:
253
+ ui_config = plugin.provides["ui"]
254
+ if isinstance(ui_config, dict):
255
+ if ui_config.get("component"):
256
+ response_data["component_url"] = f"{base_url}{ui_config['component']}"
257
+ if ui_config.get("module"):
258
+ response_data["module_url"] = f"{base_url}{ui_config['module']}"
259
+ if ui_config.get("props"):
260
+ response_data["props"] = ui_config["props"]
261
+
262
+ return response_data
\ No newline at end of file
python/helpers/projects.py
+2
-1
@@ -479,7 +479,8 @@ def create_project_meta_folders(name: str):
479
480
# create knowledge folders
481
files.create_dir(get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR))
482
- from python.helpers import memory
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(
python/helpers/settings.py
+3
-1
@@ -629,7 +629,9 @@ def _apply_settings(previous: Settings | None):
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.memory import reload as memory_reload
632
+ from python.helpers.plugins import import_plugin_module
633
+ memory = import_plugin_module("memory", "helpers/memory.py")
634
+ memory_reload = memory.reload
635
636
memory_reload()
637
python/tools/behaviour_adjustment.py
+3
-1
@@ -1,4 +1,6 @@
1
-from python.helpers import files, memory
1
+from python.helpers import files
2
+from python.helpers.plugins import import_plugin_module
3
+memory = import_plugin_module("memory", "helpers/memory.py")
4
from python.helpers.tool import Tool, Response
5
from agent import Agent
6
from python.helpers.log import LogItem
python/tools/knowledge_tool._py
+6
-5
@@ -1,10 +1,11 @@
1
import asyncio
2
-from python.helpers import dotenv, memory, perplexity_search, duckduckgo_search
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
7
+
8
from python.helpers.tool import Tool, Response
4
-from python.helpers.print_style import PrintStyle
5
-from python.helpers.errors import handle_error
6
-from python.helpers.searxng import search as searxng
7
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9
from python.helpers.document_query import DocumentQueryHelper
10
11
SEARCH_ENGINE_RESULTS = 10
python/tools/search_engine.py
+3
-1
@@ -1,6 +1,8 @@
1
import os
2
import asyncio
3
-from python.helpers import dotenv, memory, perplexity_search, duckduckgo_search
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")
6
from python.helpers.tool import Tool, Response
7
from python.helpers.print_style import PrintStyle
8
from python.helpers.errors import handle_error
run_ui.py
+43
@@ -237,6 +237,41 @@ async def serve_index():
237
return index
238
239
240
+# Serve plugin assets
241
+@webapp.route("/plugins/<plugin_id>/<path:asset_path>", methods=["GET"])
242
+@requires_auth
243
+async def serve_plugin_asset(plugin_id, asset_path):
244
+ """
245
+ Serve static assets from plugin directories.
246
+ Resolves using the plugin system (with overrides).
247
+ """
248
+ from python.helpers import plugins
249
+ from flask import send_file
250
+
251
+ # Use the new find_plugin helper
252
+ plugin = plugins.find_plugin(plugin_id)
253
+ if not plugin:
254
+ return Response("Plugin not found", 404)
255
+
256
+ # Resolve the plugin asset path with security checks
257
+ try:
258
+ # Construct path using plugin root
259
+ asset_file = (plugin.path / asset_path).resolve()
260
+ plugin_root = plugin.path.resolve()
261
+
262
+ # Security: ensure the resolved path is within the plugin directory
263
+ if not str(asset_file).startswith(str(plugin_root) + os.sep) and str(asset_file) != str(plugin_root):
264
+ return Response("Access denied", 403)
265
+
266
+ if not asset_file.is_file():
267
+ return Response("Asset not found", 404)
268
+
269
+ return send_file(str(asset_file))
270
+ except Exception as e:
271
+ PrintStyle.error(f"Error serving plugin asset: {e}")
272
+ return Response("Error serving asset", 500)
273
+
274
+
275
def _build_websocket_handlers_by_namespace(
276
socketio_server: socketio.AsyncServer,
277
lock: threading.RLock,
@@ -459,6 +494,14 @@ def run():
494
handlers = load_classes_from_folder("python/api", "*.py", ApiHandler)
495
for handler in handlers:
496
register_api_handler(webapp, handler)
497
+
498
+ # Load API handlers from plugins
499
+ 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)
503
+ for handler in plugin_handlers:
504
+ register_api_handler(webapp, handler)
505
506
handlers_by_namespace = _build_websocket_handlers_by_namespace(socketio_server, lock)
507
configure_websocket_namespaces(
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('modals/memory/memory-dashboard.html');"
37
+ @click="openModal('../plugins/memory/ui/memory-dashboard.html');"
38
>
39
Open Dashboard
40
</button>
webui/components/sidebar/top-section/quick-actions.html
+2
-2
@@ -17,7 +17,7 @@
17
</button>
18
19
<!-- Memory -->
20
- <button class="config-button" id="memory-dash" @click="openModal('modals/memory/memory-dashboard.html')" title="Memory">
20
+ <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/ui/memory-dashboard.html')" title="Memory">
21
<span class="material-symbols-outlined">psychology</span>
22
</button>
23
@@ -54,7 +54,7 @@
54
<span>Projects</span>
55
</button>
56
57
- <button class="dropdown-item" @click="openModal('modals/memory/memory-dashboard.html'); dropdownOpen = false">
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>
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 "/components/modals/memory/memory-dashboard-store.js";
4
+import { store as memoryStore } from "/plugins/memory/ui/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/components.js
+1
-1
@@ -261,4 +261,4 @@ const observer = new MutationObserver((mutations) => {
261
}
262
}
263
});
264
-observer.observe(document.body, { childList: true, subtree: true });
264
+observer.observe(document.body, { childList: true, subtree: true });
\ No newline at end of file
webui/js/initFw.js
+1
@@ -1,6 +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";
5
import { registerAlpineMagic } from "./confirmClick.js";
6
7
// initialize required elements
webui/js/plugins.js
new
+215
@@ -0,0 +1,215 @@
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]) {
129
+ 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
+ }
149
+ }
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
+ }
213
+ }
214
+});
215
+observer.observe(document.body, { childList: true, subtree: true });