@yamo/memory-mesh 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/lib/llm/client.js +391 -0
- package/lib/llm/index.js +10 -0
- package/lib/memory/memory-mesh.js +267 -15
- package/lib/search/keyword-search.js +3 -3
- package/lib/yamo/emitter.js +235 -0
- package/lib/yamo/index.js +15 -0
- package/lib/yamo/schema.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Built on the [YAMO Protocol](https://github.com/yamo-protocol) for transparent a
|
|
|
12
12
|
- **Portable CLI**: Simple JSON-based interface for any agent or language.
|
|
13
13
|
- **YAMO Skills Integration**: Includes yamo-super workflow system with automatic memory learning.
|
|
14
14
|
- **Pattern Recognition**: Workflows automatically store and retrieve execution patterns for optimization.
|
|
15
|
+
- **LLM-Powered Reflections**: Generate insights from memories using configurable LLM providers.
|
|
16
|
+
- **YAMO Audit Trail**: Automatic emission of structured blocks for all memory operations.
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -44,6 +46,63 @@ await mesh.add('Content', { meta: 'data' });
|
|
|
44
46
|
const results = await mesh.search('query');
|
|
45
47
|
```
|
|
46
48
|
|
|
49
|
+
### Enhanced Reflections with LLM
|
|
50
|
+
|
|
51
|
+
MemoryMesh supports LLM-powered reflection generation that synthesizes insights from stored memories:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { MemoryMesh } from '@yamo/memory-mesh';
|
|
55
|
+
|
|
56
|
+
// Enable LLM integration (requires API key or local model)
|
|
57
|
+
const mesh = new MemoryMesh({
|
|
58
|
+
enableLLM: true,
|
|
59
|
+
llmProvider: 'openai', // or 'anthropic', 'ollama'
|
|
60
|
+
llmApiKey: process.env.OPENAI_API_KEY,
|
|
61
|
+
llmModel: 'gpt-4o-mini'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Store some memories
|
|
65
|
+
await mesh.add('Bug: type mismatch in keyword search', { type: 'bug' });
|
|
66
|
+
await mesh.add('Bug: missing content field', { type: 'bug' });
|
|
67
|
+
|
|
68
|
+
// Generate reflection (automatically stores result to memory)
|
|
69
|
+
const reflection = await mesh.reflect({ topic: 'bugs', lookback: 10 });
|
|
70
|
+
|
|
71
|
+
console.log(reflection.reflection);
|
|
72
|
+
// Output: "Synthesized from 2 memories: Bug: type mismatch..., Bug: missing content..."
|
|
73
|
+
|
|
74
|
+
console.log(reflection.confidence); // 0.85
|
|
75
|
+
console.log(reflection.yamoBlock); // YAMO audit trail
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**CLI Usage:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# With LLM (default)
|
|
82
|
+
memory-mesh reflect '{"topic": "bugs", "limit": 10}'
|
|
83
|
+
|
|
84
|
+
# Without LLM (prompt-only for external LLM)
|
|
85
|
+
memory-mesh reflect '{"topic": "bugs", "llm": false}'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### YAMO Audit Trail
|
|
89
|
+
|
|
90
|
+
MemoryMesh automatically emits YAMO blocks for all operations when enabled:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
const mesh = new MemoryMesh({ enableYamo: true });
|
|
94
|
+
|
|
95
|
+
// All operations now emit YAMO blocks
|
|
96
|
+
await mesh.add('Memory content', { type: 'event' }); // emits 'retain' block
|
|
97
|
+
await mesh.search('query'); // emits 'recall' block
|
|
98
|
+
await mesh.reflect({ topic: 'test' }); // emits 'reflect' block
|
|
99
|
+
|
|
100
|
+
// Query YAMO log
|
|
101
|
+
const yamoLog = await mesh.getYamoLog({ operationType: 'reflect', limit: 10 });
|
|
102
|
+
console.log(yamoLog);
|
|
103
|
+
// [{ id, agentId, operationType, yamoText, timestamp, ... }]
|
|
104
|
+
```
|
|
105
|
+
|
|
47
106
|
## Using in a Project
|
|
48
107
|
|
|
49
108
|
To use MemoryMesh with your Claude Code skills (like `yamo-super`) in a new project:
|
|
@@ -127,3 +186,66 @@ Memory Mesh implements YAMO v2.1.0 compliance with:
|
|
|
127
186
|
- **Development Guide**: [CLAUDE.md](CLAUDE.md) - Guide for Claude Code development
|
|
128
187
|
- **Marketplace**: [.claude-plugin/marketplace.json](.claude-plugin/marketplace.json) - Plugin metadata
|
|
129
188
|
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
### LLM Provider Configuration
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Required for LLM-powered reflections
|
|
195
|
+
LLM_PROVIDER=openai # Provider: 'openai', 'anthropic', 'ollama'
|
|
196
|
+
LLM_API_KEY=sk-... # API key for OpenAI/Anthropic
|
|
197
|
+
LLM_MODEL=gpt-4o-mini # Model name
|
|
198
|
+
LLM_BASE_URL=https://... # Optional: Custom API base URL
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Supported Providers:**
|
|
202
|
+
- **OpenAI**: GPT-4, GPT-4o-mini, etc.
|
|
203
|
+
- **Anthropic**: Claude 3.5 Haiku, Sonnet, Opus
|
|
204
|
+
- **Ollama**: Local models (llama3.2, mistral, etc.)
|
|
205
|
+
|
|
206
|
+
### YAMO Configuration
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Optional YAMO settings
|
|
210
|
+
ENABLE_YAMO=true # Enable YAMO block emission (default: true)
|
|
211
|
+
YAMO_DEBUG=true # Enable verbose YAMO logging
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### LanceDB Configuration
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Vector database settings
|
|
218
|
+
LANCEDB_URI=./runtime/data/lancedb
|
|
219
|
+
LANCEDB_MEMORY_TABLE=memory_entries
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Embedding Configuration
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Embedding model settings
|
|
226
|
+
EMBEDDING_MODEL_TYPE=local # 'local', 'openai', 'cohere', 'ollama'
|
|
227
|
+
EMBEDDING_MODEL_NAME=Xenova/all-MiniLM-L6-v2
|
|
228
|
+
EMBEDDING_DIMENSION=384
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Example .env File
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# LLM for reflections
|
|
235
|
+
LLM_PROVIDER=openai
|
|
236
|
+
LLM_API_KEY=sk-your-key-here
|
|
237
|
+
LLM_MODEL=gpt-4o-mini
|
|
238
|
+
|
|
239
|
+
# YAMO audit
|
|
240
|
+
ENABLE_YAMO=true
|
|
241
|
+
YAMO_DEBUG=false
|
|
242
|
+
|
|
243
|
+
# Vector DB
|
|
244
|
+
LANCEDB_URI=./data/lancedb
|
|
245
|
+
|
|
246
|
+
# Embeddings (local default)
|
|
247
|
+
EMBEDDING_MODEL_TYPE=local
|
|
248
|
+
EMBEDDING_MODEL_NAME=Xenova/all-MiniLM-L6-v2
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Client - Multi-provider LLM API client for reflection generation
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - OpenAI (GPT-4, GPT-4o-mini, etc.)
|
|
6
|
+
* - Anthropic (Claude)
|
|
7
|
+
* - Ollama (local models)
|
|
8
|
+
* - Graceful fallback when LLM unavailable
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* LLMClient provides unified interface for calling various LLM providers
|
|
13
|
+
* to generate reflections from memory contexts.
|
|
14
|
+
*/
|
|
15
|
+
export class LLMClient {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new LLMClient instance
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} [config={}] - Configuration options
|
|
20
|
+
* @param {string} [config.provider='openai'] - LLM provider ('openai', 'anthropic', 'ollama')
|
|
21
|
+
* @param {string} [config.apiKey] - API key (defaults to env var)
|
|
22
|
+
* @param {string} [config.model] - Model name
|
|
23
|
+
* @param {string} [config.baseUrl] - Base URL for API (optional)
|
|
24
|
+
* @param {number} [config.timeout=30000] - Request timeout in ms
|
|
25
|
+
* @param {number} [config.maxRetries=2] - Max retry attempts
|
|
26
|
+
*/
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
this.provider = config.provider || process.env.LLM_PROVIDER || 'openai';
|
|
29
|
+
this.apiKey = config.apiKey || process.env.LLM_API_KEY || '';
|
|
30
|
+
this.model = config.model || process.env.LLM_MODEL || this._getDefaultModel();
|
|
31
|
+
this.baseUrl = config.baseUrl || process.env.LLM_BASE_URL || this._getDefaultBaseUrl();
|
|
32
|
+
this.timeout = config.timeout || 30000;
|
|
33
|
+
this.maxRetries = config.maxRetries || 2;
|
|
34
|
+
|
|
35
|
+
// Statistics
|
|
36
|
+
this.stats = {
|
|
37
|
+
totalRequests: 0,
|
|
38
|
+
successfulRequests: 0,
|
|
39
|
+
failedRequests: 0,
|
|
40
|
+
fallbackCount: 0
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get default model for provider
|
|
46
|
+
* @private
|
|
47
|
+
* @returns {string} Default model name
|
|
48
|
+
*/
|
|
49
|
+
_getDefaultModel() {
|
|
50
|
+
const defaults = {
|
|
51
|
+
openai: 'gpt-4o-mini',
|
|
52
|
+
anthropic: 'claude-3-5-haiku-20241022',
|
|
53
|
+
ollama: 'llama3.2'
|
|
54
|
+
};
|
|
55
|
+
return defaults[this.provider] || 'gpt-4o-mini';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get default base URL for provider
|
|
60
|
+
* @private
|
|
61
|
+
* @returns {string} Default base URL
|
|
62
|
+
*/
|
|
63
|
+
_getDefaultBaseUrl() {
|
|
64
|
+
const defaults = {
|
|
65
|
+
openai: 'https://api.openai.com/v1',
|
|
66
|
+
anthropic: 'https://api.anthropic.com/v1',
|
|
67
|
+
ollama: 'http://localhost:11434'
|
|
68
|
+
};
|
|
69
|
+
return defaults[this.provider] || 'https://api.openai.com/v1';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate reflection from memories
|
|
74
|
+
* Main entry point for reflection generation
|
|
75
|
+
*
|
|
76
|
+
* @param {string} prompt - The reflection prompt
|
|
77
|
+
* @param {Array} memories - Context memories
|
|
78
|
+
* @returns {Promise<Object>} { reflection, confidence }
|
|
79
|
+
*/
|
|
80
|
+
async reflect(prompt, memories) {
|
|
81
|
+
this.stats.totalRequests++;
|
|
82
|
+
|
|
83
|
+
if (!memories || memories.length === 0) {
|
|
84
|
+
return this._fallback('No memories provided');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const systemPrompt = `You are a reflective AI agent. Review the provided memories and synthesize a high-level insight, belief, or observation.
|
|
88
|
+
Respond ONLY in JSON format with exactly these keys:
|
|
89
|
+
{
|
|
90
|
+
"reflection": "a concise insight or observation derived from the memories",
|
|
91
|
+
"confidence": 0.0 to 1.0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Keep the reflection brief (1-2 sentences) and actionable.`;
|
|
95
|
+
|
|
96
|
+
const userContent = this._formatMemoriesForLLM(prompt, memories);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await this._callWithRetry(systemPrompt, userContent);
|
|
100
|
+
const parsed = JSON.parse(response);
|
|
101
|
+
|
|
102
|
+
// Validate response structure
|
|
103
|
+
if (!parsed.reflection || typeof parsed.confidence !== 'number') {
|
|
104
|
+
throw new Error('Invalid LLM response format');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Clamp confidence to valid range
|
|
108
|
+
parsed.confidence = Math.max(0, Math.min(1, parsed.confidence));
|
|
109
|
+
|
|
110
|
+
this.stats.successfulRequests++;
|
|
111
|
+
return parsed;
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.stats.failedRequests++;
|
|
115
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
+
console.warn(`[LLMClient] LLM call failed: ${errorMessage}`);
|
|
117
|
+
return this._fallback('LLM error', memories);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format memories for LLM consumption
|
|
123
|
+
* @private
|
|
124
|
+
* @param {string} prompt - User prompt
|
|
125
|
+
* @param {Array} memories - Memory array
|
|
126
|
+
* @returns {string} Formatted content
|
|
127
|
+
*/
|
|
128
|
+
_formatMemoriesForLLM(prompt, memories) {
|
|
129
|
+
const memoryList = memories
|
|
130
|
+
.map((m, i) => `${i + 1}. ${m.content}`)
|
|
131
|
+
.join('\n');
|
|
132
|
+
|
|
133
|
+
return `Prompt: ${prompt}\n\nMemories:\n${memoryList}\n\nBased on these memories, provide a brief reflective insight.`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Call LLM with retry logic
|
|
138
|
+
* @private
|
|
139
|
+
* @param {string} systemPrompt - System prompt
|
|
140
|
+
* @param {string} userContent - User content
|
|
141
|
+
* @returns {Promise<string>} LLM response text
|
|
142
|
+
*/
|
|
143
|
+
async _callWithRetry(systemPrompt, userContent) {
|
|
144
|
+
let lastError = null;
|
|
145
|
+
|
|
146
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
return await this._callLLM(systemPrompt, userContent);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
lastError = error;
|
|
151
|
+
if (attempt < this.maxRetries) {
|
|
152
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
153
|
+
await this._sleep(delay);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw lastError;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Call LLM based on provider
|
|
163
|
+
* @private
|
|
164
|
+
* @param {string} systemPrompt - System prompt
|
|
165
|
+
* @param {string} userContent - User content
|
|
166
|
+
* @returns {Promise<string>} Response text
|
|
167
|
+
*/
|
|
168
|
+
async _callLLM(systemPrompt, userContent) {
|
|
169
|
+
switch (this.provider) {
|
|
170
|
+
case 'openai':
|
|
171
|
+
return await this._callOpenAI(systemPrompt, userContent);
|
|
172
|
+
case 'anthropic':
|
|
173
|
+
return await this._callAnthropic(systemPrompt, userContent);
|
|
174
|
+
case 'ollama':
|
|
175
|
+
return await this._callOllama(systemPrompt, userContent);
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Unsupported provider: ${this.provider}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Call OpenAI API
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
async _callOpenAI(systemPrompt, userContent) {
|
|
186
|
+
if (!this.apiKey) {
|
|
187
|
+
throw new Error('OpenAI API key not configured');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
model: this.model,
|
|
202
|
+
messages: [
|
|
203
|
+
{ role: 'system', content: systemPrompt },
|
|
204
|
+
{ role: 'user', content: userContent }
|
|
205
|
+
],
|
|
206
|
+
temperature: 0.7,
|
|
207
|
+
max_tokens: 500
|
|
208
|
+
}),
|
|
209
|
+
signal: controller.signal
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
const error = await response.text();
|
|
216
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const data = await response.json();
|
|
220
|
+
return data.choices[0].message.content;
|
|
221
|
+
|
|
222
|
+
} catch (error) {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
225
|
+
throw new Error('Request timeout');
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Call Anthropic (Claude) API
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
async _callAnthropic(systemPrompt, userContent) {
|
|
236
|
+
if (!this.apiKey) {
|
|
237
|
+
throw new Error('Anthropic API key not configured');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetch(`${this.baseUrl}/messages`, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: {
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
'x-api-key': this.apiKey,
|
|
249
|
+
'anthropic-version': '2023-06-01'
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
model: this.model,
|
|
253
|
+
max_tokens: 500,
|
|
254
|
+
system: systemPrompt,
|
|
255
|
+
messages: [
|
|
256
|
+
{ role: 'user', content: userContent }
|
|
257
|
+
]
|
|
258
|
+
}),
|
|
259
|
+
signal: controller.signal
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
clearTimeout(timeoutId);
|
|
263
|
+
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const error = await response.text();
|
|
266
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = await response.json();
|
|
270
|
+
return data.content[0].text;
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
clearTimeout(timeoutId);
|
|
274
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
275
|
+
throw new Error('Request timeout');
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Call Ollama (local) API
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
async _callOllama(systemPrompt, userContent) {
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: {
|
|
293
|
+
'Content-Type': 'application/json'
|
|
294
|
+
},
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
model: this.model,
|
|
297
|
+
messages: [
|
|
298
|
+
{ role: 'system', content: systemPrompt },
|
|
299
|
+
{ role: 'user', content: userContent }
|
|
300
|
+
],
|
|
301
|
+
stream: false
|
|
302
|
+
}),
|
|
303
|
+
signal: controller.signal
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
clearTimeout(timeoutId);
|
|
307
|
+
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
const error = await response.text();
|
|
310
|
+
throw new Error(`Ollama API error: ${response.status} - ${error}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const data = await response.json();
|
|
314
|
+
return data.message.content;
|
|
315
|
+
|
|
316
|
+
} catch (error) {
|
|
317
|
+
clearTimeout(timeoutId);
|
|
318
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
319
|
+
throw new Error('Request timeout');
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Fallback when LLM fails
|
|
327
|
+
* @private
|
|
328
|
+
* @param {string} reason - Fallback reason
|
|
329
|
+
* @param {Array} [memories=[]] - Memory array
|
|
330
|
+
* @returns {Object} Fallback result
|
|
331
|
+
*/
|
|
332
|
+
_fallback(reason, memories = []) {
|
|
333
|
+
this.stats.fallbackCount++;
|
|
334
|
+
|
|
335
|
+
if (memories && memories.length > 0) {
|
|
336
|
+
// Simple aggregation fallback
|
|
337
|
+
const contents = memories.map(m => m.content);
|
|
338
|
+
const combined = contents.join('; ');
|
|
339
|
+
const preview = combined.length > 200
|
|
340
|
+
? combined.substring(0, 200) + '...'
|
|
341
|
+
: combined;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
reflection: `Aggregated from ${memories.length} memories: ${preview}`,
|
|
345
|
+
confidence: 0.5
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
reflection: `Reflection generation unavailable: ${reason}`,
|
|
351
|
+
confidence: 0.3
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Sleep utility
|
|
357
|
+
* @private
|
|
358
|
+
* @param {number} ms - Milliseconds to sleep
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
_sleep(ms) {
|
|
362
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get client statistics
|
|
367
|
+
* @returns {Object} Statistics
|
|
368
|
+
*/
|
|
369
|
+
getStats() {
|
|
370
|
+
return {
|
|
371
|
+
...this.stats,
|
|
372
|
+
successRate: this.stats.totalRequests > 0
|
|
373
|
+
? (this.stats.successfulRequests / this.stats.totalRequests).toFixed(2)
|
|
374
|
+
: '0.00'
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Reset statistics
|
|
380
|
+
*/
|
|
381
|
+
resetStats() {
|
|
382
|
+
this.stats = {
|
|
383
|
+
totalRequests: 0,
|
|
384
|
+
successfulRequests: 0,
|
|
385
|
+
failedRequests: 0,
|
|
386
|
+
fallbackCount: 0
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default LLMClient;
|
package/lib/llm/index.js
ADDED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import fs from "fs";
|
|
18
|
+
import crypto from "crypto";
|
|
18
19
|
import { LanceDBClient } from "../lancedb/client.js";
|
|
19
20
|
import { getConfig } from "../lancedb/config.js";
|
|
20
21
|
import { getEmbeddingDimension } from "../lancedb/schema.js";
|
|
@@ -22,6 +23,8 @@ import { handleError, StorageError, QueryError } from "../lancedb/errors.js";
|
|
|
22
23
|
import EmbeddingFactory from "../embeddings/factory.js";
|
|
23
24
|
import { Scrubber } from "../scrubber/scrubber.js";
|
|
24
25
|
import { KeywordSearch } from "../search/keyword-search.js";
|
|
26
|
+
import { YamoEmitter } from "../yamo/emitter.js";
|
|
27
|
+
import { LLMClient } from "../llm/client.js";
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* MemoryMesh class for managing vector memory storage
|
|
@@ -29,8 +32,15 @@ import { KeywordSearch } from "../search/keyword-search.js";
|
|
|
29
32
|
class MemoryMesh {
|
|
30
33
|
/**
|
|
31
34
|
* Create a new MemoryMesh instance
|
|
35
|
+
* @param {Object} [options={}] - Configuration options
|
|
36
|
+
* @param {boolean} [options.enableYamo=true] - Enable YAMO block emission
|
|
37
|
+
* @param {boolean} [options.enableLLM=true] - Enable LLM for reflections
|
|
38
|
+
* @param {string} [options.agentId='default'] - Agent identifier for YAMO blocks
|
|
39
|
+
* @param {string} [options.llmProvider] - LLM provider (openai, anthropic, ollama)
|
|
40
|
+
* @param {string} [options.llmApiKey] - LLM API key
|
|
41
|
+
* @param {string} [options.llmModel] - LLM model name
|
|
32
42
|
*/
|
|
33
|
-
constructor() {
|
|
43
|
+
constructor(options = {}) {
|
|
34
44
|
this.client = null;
|
|
35
45
|
this.config = null;
|
|
36
46
|
this.embeddingFactory = new EmbeddingFactory();
|
|
@@ -38,8 +48,24 @@ class MemoryMesh {
|
|
|
38
48
|
this.isInitialized = false;
|
|
39
49
|
this.vectorDimension = 384; // Will be set during init()
|
|
40
50
|
|
|
51
|
+
// YAMO and LLM support
|
|
52
|
+
this.enableYamo = options.enableYamo !== false; // Default: true
|
|
53
|
+
this.enableLLM = options.enableLLM !== false; // Default: true
|
|
54
|
+
this.agentId = options.agentId || 'default';
|
|
55
|
+
this.yamoTable = null; // Will be initialized in init()
|
|
56
|
+
this.llmClient = null;
|
|
57
|
+
|
|
58
|
+
// Initialize LLM client if enabled
|
|
59
|
+
if (this.enableLLM) {
|
|
60
|
+
this.llmClient = new LLMClient({
|
|
61
|
+
provider: options.llmProvider,
|
|
62
|
+
apiKey: options.llmApiKey,
|
|
63
|
+
model: options.llmModel
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
// Scrubber for Layer 0 sanitization
|
|
42
|
-
this.scrubber = new Scrubber({
|
|
68
|
+
this.scrubber = new Scrubber({
|
|
43
69
|
enabled: true,
|
|
44
70
|
chunking: {
|
|
45
71
|
minTokens: 1 // Allow short memories
|
|
@@ -235,6 +261,20 @@ class MemoryMesh {
|
|
|
235
261
|
}
|
|
236
262
|
}
|
|
237
263
|
|
|
264
|
+
// Initialize YAMO blocks table if enabled
|
|
265
|
+
if (this.enableYamo && this.client && this.client.db) {
|
|
266
|
+
try {
|
|
267
|
+
const { createYamoTable } = await import('../yamo/schema.js');
|
|
268
|
+
this.yamoTable = await createYamoTable(this.client.db, 'yamo_blocks');
|
|
269
|
+
if (process.env.YAMO_DEBUG === 'true') {
|
|
270
|
+
console.error('[MemoryMesh] YAMO blocks table initialized');
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// Log warning but don't fail initialization
|
|
274
|
+
console.warn('[MemoryMesh] Failed to initialize YAMO table:', e instanceof Error ? e.message : String(e));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
238
278
|
this.isInitialized = true;
|
|
239
279
|
|
|
240
280
|
} catch (error) {
|
|
@@ -314,6 +354,22 @@ class MemoryMesh {
|
|
|
314
354
|
// Add to Keyword Search
|
|
315
355
|
this.keywordSearch.add(record.id, record.content, sanitizedMetadata);
|
|
316
356
|
|
|
357
|
+
// Emit YAMO block for retain operation (async, non-blocking)
|
|
358
|
+
if (this.enableYamo) {
|
|
359
|
+
// Fire and forget - don't await
|
|
360
|
+
this._emitYamoBlock('retain', result.id, YamoEmitter.buildRetainBlock({
|
|
361
|
+
content: sanitizedContent,
|
|
362
|
+
metadata: sanitizedMetadata,
|
|
363
|
+
id: result.id,
|
|
364
|
+
agentId: this.agentId,
|
|
365
|
+
memoryType: sanitizedMetadata.type || 'event'
|
|
366
|
+
})).catch(err => {
|
|
367
|
+
if (process.env.YAMO_DEBUG === 'true') {
|
|
368
|
+
console.error('[MemoryMesh] YAMO emission failed in add():', err);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
317
373
|
return {
|
|
318
374
|
id: result.id,
|
|
319
375
|
content: sanitizedContent,
|
|
@@ -329,15 +385,21 @@ class MemoryMesh {
|
|
|
329
385
|
}
|
|
330
386
|
|
|
331
387
|
/**
|
|
332
|
-
* Reflect on recent memories to generate insights
|
|
333
|
-
* @param {Object} options
|
|
334
|
-
* @
|
|
388
|
+
* Reflect on recent memories to generate insights (enhanced with LLM + YAMO)
|
|
389
|
+
* @param {Object} options
|
|
390
|
+
* @param {string} [options.topic] - Topic to search for
|
|
391
|
+
* @param {number} [options.lookback=10] - Number of memories to consider
|
|
392
|
+
* @param {boolean} [options.generate=true] - Whether to generate reflection via LLM
|
|
393
|
+
* @returns {Promise<Object>} Reflection result with YAMO block
|
|
335
394
|
*/
|
|
336
395
|
async reflect(options = {}) {
|
|
337
396
|
await this.init();
|
|
397
|
+
|
|
338
398
|
const lookback = options.lookback || 10;
|
|
339
399
|
const topic = options.topic;
|
|
400
|
+
const generate = options.generate !== false;
|
|
340
401
|
|
|
402
|
+
// Gather memories
|
|
341
403
|
let memories = [];
|
|
342
404
|
if (topic) {
|
|
343
405
|
memories = await this.search(topic, { limit: lookback });
|
|
@@ -348,18 +410,115 @@ class MemoryMesh {
|
|
|
348
410
|
.slice(0, lookback);
|
|
349
411
|
}
|
|
350
412
|
|
|
413
|
+
const prompt = `Review these memories. Synthesize a high-level "belief" or "observation".`;
|
|
414
|
+
|
|
415
|
+
// Check if LLM generation is requested and available
|
|
416
|
+
if (!generate || !this.enableLLM || !this.llmClient) {
|
|
417
|
+
// Return prompt-only mode (backward compatible)
|
|
418
|
+
return {
|
|
419
|
+
topic,
|
|
420
|
+
count: memories.length,
|
|
421
|
+
context: memories.map(m => ({
|
|
422
|
+
content: m.content,
|
|
423
|
+
type: m.metadata?.type || 'event',
|
|
424
|
+
id: m.id
|
|
425
|
+
})),
|
|
426
|
+
prompt
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Generate reflection via LLM
|
|
431
|
+
let reflection = null;
|
|
432
|
+
let confidence = 0;
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const result = await this.llmClient.reflect(prompt, memories);
|
|
436
|
+
reflection = result.reflection;
|
|
437
|
+
confidence = result.confidence;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
440
|
+
console.warn(`[MemoryMesh] LLM reflection failed: ${errorMessage}`);
|
|
441
|
+
// Fall back to simple aggregation
|
|
442
|
+
reflection = `Aggregated from ${memories.length} memories on topic: ${topic || 'general'}`;
|
|
443
|
+
confidence = 0.5;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Store reflection to memory
|
|
447
|
+
const reflectionId = `reflect_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
448
|
+
await this.add(reflection, {
|
|
449
|
+
type: 'reflection',
|
|
450
|
+
topic: topic || 'general',
|
|
451
|
+
source_memory_count: memories.length,
|
|
452
|
+
confidence,
|
|
453
|
+
generated_at: new Date().toISOString()
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Emit YAMO block if enabled
|
|
457
|
+
let yamoBlock = null;
|
|
458
|
+
if (this.enableYamo) {
|
|
459
|
+
yamoBlock = YamoEmitter.buildReflectBlock({
|
|
460
|
+
topic: topic || 'general',
|
|
461
|
+
memoryCount: memories.length,
|
|
462
|
+
agentId: this.agentId,
|
|
463
|
+
reflection,
|
|
464
|
+
confidence
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await this._emitYamoBlock('reflect', reflectionId, yamoBlock);
|
|
468
|
+
}
|
|
469
|
+
|
|
351
470
|
return {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
prompt: `Review these memories. Synthesize a high-level "belief" or "observation".`
|
|
471
|
+
id: reflectionId,
|
|
472
|
+
topic: topic || 'general',
|
|
473
|
+
reflection,
|
|
474
|
+
confidence,
|
|
475
|
+
sourceMemoryCount: memories.length,
|
|
476
|
+
yamoBlock,
|
|
477
|
+
createdAt: new Date().toISOString()
|
|
360
478
|
};
|
|
361
479
|
}
|
|
362
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Emit a YAMO block to the YAMO blocks table
|
|
483
|
+
* @private
|
|
484
|
+
* @param {string} operationType - 'retain', 'recall', 'reflect'
|
|
485
|
+
* @param {string|undefined} memoryId - Associated memory ID (undefined for recall)
|
|
486
|
+
* @param {string} yamoText - The YAMO block text
|
|
487
|
+
*/
|
|
488
|
+
async _emitYamoBlock(operationType, memoryId, yamoText) {
|
|
489
|
+
if (!this.yamoTable) {
|
|
490
|
+
if (process.env.YAMO_DEBUG === 'true') {
|
|
491
|
+
console.warn('[MemoryMesh] YAMO table not initialized, skipping emission');
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const yamoId = `yamo_${operationType}_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await this.yamoTable.add([{
|
|
500
|
+
id: yamoId,
|
|
501
|
+
agent_id: this.agentId,
|
|
502
|
+
operation_type: operationType,
|
|
503
|
+
yamo_text: yamoText,
|
|
504
|
+
timestamp: new Date(),
|
|
505
|
+
block_hash: null, // Future: blockchain anchoring
|
|
506
|
+
prev_hash: null,
|
|
507
|
+
metadata: JSON.stringify({
|
|
508
|
+
memory_id: memoryId || null,
|
|
509
|
+
timestamp: new Date().toISOString()
|
|
510
|
+
})
|
|
511
|
+
}]);
|
|
512
|
+
|
|
513
|
+
if (process.env.YAMO_DEBUG === 'true') {
|
|
514
|
+
console.log(`[MemoryMesh] YAMO block emitted: ${yamoId}`);
|
|
515
|
+
}
|
|
516
|
+
} catch (error) {
|
|
517
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
518
|
+
console.error(`[MemoryMesh] Failed to emit YAMO block: ${errorMessage}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
363
522
|
/**
|
|
364
523
|
* Add multiple memory entries in batch for efficiency
|
|
365
524
|
* @param {Array<{content: string, metadata?: Object}>} entries - Array of entries to add
|
|
@@ -521,6 +680,21 @@ class MemoryMesh {
|
|
|
521
680
|
this._cacheResult(cacheKey, mergedResults);
|
|
522
681
|
}
|
|
523
682
|
|
|
683
|
+
// Emit YAMO block for recall operation (async, non-blocking)
|
|
684
|
+
if (this.enableYamo) {
|
|
685
|
+
this._emitYamoBlock('recall', undefined, YamoEmitter.buildRecallBlock({
|
|
686
|
+
query,
|
|
687
|
+
resultCount: mergedResults.length,
|
|
688
|
+
limit,
|
|
689
|
+
agentId: this.agentId,
|
|
690
|
+
searchType: 'hybrid'
|
|
691
|
+
})).catch(err => {
|
|
692
|
+
if (process.env.YAMO_DEBUG === 'true') {
|
|
693
|
+
console.error('[MemoryMesh] YAMO emission failed in search():', err);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
524
698
|
return mergedResults;
|
|
525
699
|
|
|
526
700
|
} catch (error) {
|
|
@@ -577,6 +751,62 @@ class MemoryMesh {
|
|
|
577
751
|
}
|
|
578
752
|
}
|
|
579
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Get YAMO blocks for this agent (audit trail)
|
|
756
|
+
* @param {Object} options - Query options
|
|
757
|
+
* @param {string} [options.operationType] - Filter by operation type ('retain', 'recall', 'reflect')
|
|
758
|
+
* @param {number} [options.limit=10] - Max results to return
|
|
759
|
+
* @returns {Promise<Array>} List of YAMO blocks
|
|
760
|
+
*/
|
|
761
|
+
async getYamoLog(options = {}) {
|
|
762
|
+
if (!this.yamoTable) {
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const limit = options.limit || 10;
|
|
767
|
+
const operationType = options.operationType;
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
// Use search with empty vector to get all records, then filter
|
|
771
|
+
// This avoids using the protected execute() method
|
|
772
|
+
const allResults = [];
|
|
773
|
+
|
|
774
|
+
// Build query manually using the LanceDB table
|
|
775
|
+
// @ts-ignore - LanceDB types may not match exactly
|
|
776
|
+
const table = this.yamoTable;
|
|
777
|
+
|
|
778
|
+
// Get all records and filter
|
|
779
|
+
// @ts-ignore
|
|
780
|
+
const records = await table.query().limit(limit * 2).toArrow();
|
|
781
|
+
|
|
782
|
+
// Process Arrow table
|
|
783
|
+
for (const row of records) {
|
|
784
|
+
const opType = row.operationType;
|
|
785
|
+
if (!operationType || opType === operationType) {
|
|
786
|
+
allResults.push({
|
|
787
|
+
id: row.id,
|
|
788
|
+
agentId: row.agentId,
|
|
789
|
+
operationType: row.operationType,
|
|
790
|
+
yamoText: row.yamoText,
|
|
791
|
+
timestamp: row.timestamp,
|
|
792
|
+
blockHash: row.blockHash,
|
|
793
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
if (allResults.length >= limit) {
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return allResults;
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
805
|
+
console.error('[MemoryMesh] Failed to get YAMO log:', errorMessage);
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
580
810
|
/**
|
|
581
811
|
* Update a memory record
|
|
582
812
|
* @param {string} id - Record ID
|
|
@@ -961,8 +1191,30 @@ ${jsonResult}
|
|
|
961
1191
|
console.log(JSON.stringify({ status: "ok", count: records.length, records }));
|
|
962
1192
|
|
|
963
1193
|
} else if (action === 'reflect') {
|
|
964
|
-
|
|
965
|
-
|
|
1194
|
+
// Enhanced reflect with LLM support
|
|
1195
|
+
const enableLLM = input.llm !== false; // Default true
|
|
1196
|
+
const result = await mesh.reflect({
|
|
1197
|
+
topic: input.topic,
|
|
1198
|
+
lookback: input.limit || 10,
|
|
1199
|
+
generate: enableLLM
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
if (result.reflection) {
|
|
1203
|
+
// New format with LLM-generated reflection
|
|
1204
|
+
console.log(JSON.stringify({
|
|
1205
|
+
status: "ok",
|
|
1206
|
+
reflection: result.reflection,
|
|
1207
|
+
confidence: result.confidence,
|
|
1208
|
+
id: result.id,
|
|
1209
|
+
topic: result.topic,
|
|
1210
|
+
sourceMemoryCount: result.sourceMemoryCount,
|
|
1211
|
+
yamoBlock: result.yamoBlock,
|
|
1212
|
+
createdAt: result.createdAt
|
|
1213
|
+
}));
|
|
1214
|
+
} else {
|
|
1215
|
+
// Old format for backward compatibility (prompt-only mode)
|
|
1216
|
+
console.log(JSON.stringify({ status: "ok", ...result }));
|
|
1217
|
+
}
|
|
966
1218
|
|
|
967
1219
|
} else if (action === 'stats') {
|
|
968
1220
|
const stats = await mesh.stats();
|
|
@@ -90,9 +90,9 @@ export class KeywordSearch {
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Search for query terms
|
|
93
|
-
* @param {string} query
|
|
94
|
-
* @param {Object} options
|
|
95
|
-
* @returns {Array<{id: string, score: number, matches: string[]}>}
|
|
93
|
+
* @param {string} query
|
|
94
|
+
* @param {Object} options
|
|
95
|
+
* @returns {Array<{id: string, score: number, matches: string[], content: string, metadata: Object}>}
|
|
96
96
|
*/
|
|
97
97
|
search(query, options = {}) {
|
|
98
98
|
this._computeStats();
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAMO Emitter - Constructs structured YAMO blocks for auditability
|
|
3
|
+
*
|
|
4
|
+
* Based on YAMO Protocol specification:
|
|
5
|
+
* - Semicolon-terminated key-value pairs
|
|
6
|
+
* - Agent/Intent/Context/Constraints/Meta/Output structure
|
|
7
|
+
* - Supports reflect, retain, recall operations
|
|
8
|
+
*
|
|
9
|
+
* Reference: Hindsight project's yamo_integration.py
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* YamoEmitter class for building YAMO protocol blocks
|
|
14
|
+
* YAMO (Yet Another Multi-agent Orchestration) blocks provide
|
|
15
|
+
* structured reasoning traces for AI agent operations.
|
|
16
|
+
*/
|
|
17
|
+
export class YamoEmitter {
|
|
18
|
+
/**
|
|
19
|
+
* Build a YAMO block for reflect operation
|
|
20
|
+
* Reflect operations synthesize insights from existing memories
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params - Block parameters
|
|
23
|
+
* @param {string} [params.topic] - Topic of reflection
|
|
24
|
+
* @param {number} params.memoryCount - Number of memories considered
|
|
25
|
+
* @param {string} [params.agentId='default'] - Agent identifier
|
|
26
|
+
* @param {string} params.reflection - Generated reflection text
|
|
27
|
+
* @param {number} [params.confidence=0.8] - Confidence score (0-1)
|
|
28
|
+
* @returns {string} Formatted YAMO block
|
|
29
|
+
*/
|
|
30
|
+
static buildReflectBlock(params) {
|
|
31
|
+
const {
|
|
32
|
+
topic,
|
|
33
|
+
memoryCount,
|
|
34
|
+
agentId = 'default',
|
|
35
|
+
reflection,
|
|
36
|
+
confidence = 0.8
|
|
37
|
+
} = params;
|
|
38
|
+
|
|
39
|
+
const timestamp = new Date().toISOString();
|
|
40
|
+
|
|
41
|
+
return `agent: MemoryMesh_${agentId};
|
|
42
|
+
intent: synthesize_insights_from_context;
|
|
43
|
+
context:
|
|
44
|
+
topic;${topic || 'general'};
|
|
45
|
+
memory_count;${memoryCount};
|
|
46
|
+
timestamp;${timestamp};
|
|
47
|
+
constraints:
|
|
48
|
+
hypothesis;Reflection generates new insights from existing facts;
|
|
49
|
+
priority: high;
|
|
50
|
+
output:
|
|
51
|
+
reflection;${reflection};
|
|
52
|
+
confidence;${confidence};
|
|
53
|
+
meta:
|
|
54
|
+
rationale;Synthesized from ${memoryCount} relevant memories;
|
|
55
|
+
observation;High-level belief formed from pattern recognition;
|
|
56
|
+
confidence;${confidence};
|
|
57
|
+
log: reflection_generated;timestamp;${timestamp};memories;${memoryCount};
|
|
58
|
+
handoff: End;
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a YAMO block for retain (add) operation
|
|
64
|
+
* Retain operations store new memories into the system
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} params - Block parameters
|
|
67
|
+
* @param {string} params.content - Memory content
|
|
68
|
+
* @param {Object} [params.metadata={}] - Memory metadata
|
|
69
|
+
* @param {string} params.id - Memory ID
|
|
70
|
+
* @param {string} [params.agentId='default'] - Agent identifier
|
|
71
|
+
* @param {string} [params.memoryType='event'] - Type of memory
|
|
72
|
+
* @returns {string} Formatted YAMO block
|
|
73
|
+
*/
|
|
74
|
+
static buildRetainBlock(params) {
|
|
75
|
+
const {
|
|
76
|
+
content,
|
|
77
|
+
metadata = {},
|
|
78
|
+
id,
|
|
79
|
+
agentId = 'default',
|
|
80
|
+
memoryType = 'event'
|
|
81
|
+
} = params;
|
|
82
|
+
|
|
83
|
+
const timestamp = new Date().toISOString();
|
|
84
|
+
const contentPreview = content.length > 100
|
|
85
|
+
? content.substring(0, 100) + '...'
|
|
86
|
+
: content;
|
|
87
|
+
|
|
88
|
+
// Escape semicolons in content for YAMO format
|
|
89
|
+
const escapedContent = contentPreview.replace(/;/g, ',');
|
|
90
|
+
|
|
91
|
+
return `agent: MemoryMesh_${agentId};
|
|
92
|
+
intent: store_memory_for_future_retrieval;
|
|
93
|
+
context:
|
|
94
|
+
memory_id;${id};
|
|
95
|
+
memory_type;${memoryType};
|
|
96
|
+
timestamp;${timestamp};
|
|
97
|
+
content_length;${content.length};
|
|
98
|
+
constraints:
|
|
99
|
+
hypothesis;New information should be integrated into world model;
|
|
100
|
+
priority: medium;
|
|
101
|
+
output:
|
|
102
|
+
memory_stored;${id};
|
|
103
|
+
content_preview;${escapedContent};
|
|
104
|
+
meta:
|
|
105
|
+
rationale;Memory persisted for semantic search and retrieval;
|
|
106
|
+
observation;Content vectorized and stored in LanceDB;
|
|
107
|
+
confidence;1.0;
|
|
108
|
+
log: memory_retained;timestamp;${timestamp};id;${id};type;${memoryType};
|
|
109
|
+
handoff: End;
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a YAMO block for recall (search) operation
|
|
115
|
+
* Recall operations retrieve memories based on semantic similarity
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} params - Block parameters
|
|
118
|
+
* @param {string} params.query - Search query
|
|
119
|
+
* @param {number} params.resultCount - Number of results returned
|
|
120
|
+
* @param {number} [params.limit=10] - Maximum requested results
|
|
121
|
+
* @param {string} [params.agentId='default'] - Agent identifier
|
|
122
|
+
* @param {string} [params.searchType='semantic'] - Type of search
|
|
123
|
+
* @returns {string} Formatted YAMO block
|
|
124
|
+
*/
|
|
125
|
+
static buildRecallBlock(params) {
|
|
126
|
+
const {
|
|
127
|
+
query,
|
|
128
|
+
resultCount,
|
|
129
|
+
limit = 10,
|
|
130
|
+
agentId = 'default',
|
|
131
|
+
searchType = 'semantic'
|
|
132
|
+
} = params;
|
|
133
|
+
|
|
134
|
+
const timestamp = new Date().toISOString();
|
|
135
|
+
const recallRatio = resultCount > 0 ? (resultCount / limit).toFixed(2) : '0.00';
|
|
136
|
+
|
|
137
|
+
return `agent: MemoryMesh_${agentId};
|
|
138
|
+
intent: retrieve_relevant_memories;
|
|
139
|
+
context:
|
|
140
|
+
query;${query};
|
|
141
|
+
search_type;${searchType};
|
|
142
|
+
requested_limit;${limit};
|
|
143
|
+
timestamp;${timestamp};
|
|
144
|
+
constraints:
|
|
145
|
+
hypothesis;Relevant memories retrieved based on query;
|
|
146
|
+
priority: high;
|
|
147
|
+
output:
|
|
148
|
+
results_count;${resultCount};
|
|
149
|
+
recall_ratio;${recallRatio};
|
|
150
|
+
meta:
|
|
151
|
+
rationale;Semantic search finds similar content by vector similarity;
|
|
152
|
+
observation;${resultCount} memories found matching query;
|
|
153
|
+
confidence;${resultCount > 0 ? '0.9' : '0.5'};
|
|
154
|
+
log: memory_recalled;timestamp;${timestamp};results;${resultCount};query;${query};
|
|
155
|
+
handoff: End;
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build a YAMO block for delete operation (optional)
|
|
161
|
+
* Delete operations remove memories from the system
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} params - Block parameters
|
|
164
|
+
* @param {string} params.id - Memory ID being deleted
|
|
165
|
+
* @param {string} [params.agentId='default'] - Agent identifier
|
|
166
|
+
* @param {string} [params.reason='user_request'] - Reason for deletion
|
|
167
|
+
* @returns {string} Formatted YAMO block
|
|
168
|
+
*/
|
|
169
|
+
static buildDeleteBlock(params) {
|
|
170
|
+
const {
|
|
171
|
+
id,
|
|
172
|
+
agentId = 'default',
|
|
173
|
+
reason = 'user_request'
|
|
174
|
+
} = params;
|
|
175
|
+
|
|
176
|
+
const timestamp = new Date().toISOString();
|
|
177
|
+
|
|
178
|
+
return `agent: MemoryMesh_${agentId};
|
|
179
|
+
intent: remove_memory_from_storage;
|
|
180
|
+
context:
|
|
181
|
+
memory_id;${id};
|
|
182
|
+
reason;${reason};
|
|
183
|
+
timestamp;${timestamp};
|
|
184
|
+
constraints:
|
|
185
|
+
hypothesis;Memory removal should be traceable for audit;
|
|
186
|
+
priority: low;
|
|
187
|
+
output:
|
|
188
|
+
deleted;${id};
|
|
189
|
+
meta:
|
|
190
|
+
rationale;Memory removed from vector store;
|
|
191
|
+
observation;Deletion recorded for provenance;
|
|
192
|
+
confidence;1.0;
|
|
193
|
+
log: memory_deleted;timestamp;${timestamp};id;${id};
|
|
194
|
+
handoff: End;
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate a YAMO block structure
|
|
200
|
+
* Checks for required sections and proper formatting
|
|
201
|
+
*
|
|
202
|
+
* @param {string} yamoBlock - YAMO block to validate
|
|
203
|
+
* @returns {Object} Validation result { valid, errors }
|
|
204
|
+
*/
|
|
205
|
+
static validateBlock(yamoBlock) {
|
|
206
|
+
const errors = [];
|
|
207
|
+
|
|
208
|
+
// Check for required sections
|
|
209
|
+
const requiredSections = ['agent:', 'intent:', 'context:', 'output:', 'log:'];
|
|
210
|
+
for (const section of requiredSections) {
|
|
211
|
+
if (!yamoBlock.includes(section)) {
|
|
212
|
+
errors.push(`Missing required section: ${section}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for semicolon termination
|
|
217
|
+
const lines = yamoBlock.split('\n');
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
if (trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.endsWith(';')) {
|
|
221
|
+
// Allow empty lines and comments
|
|
222
|
+
if (trimmed && !trimmed.startsWith('agent:') && !trimmed.startsWith('handoff:')) {
|
|
223
|
+
errors.push(`Line not semicolon-terminated: ${trimmed.substring(0, 50)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
valid: errors.length === 0,
|
|
230
|
+
errors
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default YamoEmitter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAMO Module - YAMO Protocol support for yamo-memory-mesh
|
|
3
|
+
* Exports YAMO block construction, validation, and schema utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { YamoEmitter } from './emitter.js';
|
|
7
|
+
export * from './schema.js';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
YamoEmitter: (await import('./emitter.js')).YamoEmitter,
|
|
11
|
+
createYamoSchema: (await import('./schema.js')).createYamoSchema,
|
|
12
|
+
createYamoTable: (await import('./schema.js')).createYamoTable,
|
|
13
|
+
validateYamoRecord: (await import('./schema.js')).validateYamoRecord,
|
|
14
|
+
generateYamoId: (await import('./schema.js')).generateYamoId
|
|
15
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAMO Block Schema Definitions for yamo-memory-mesh
|
|
3
|
+
* Uses Apache Arrow Schema format for LanceDB JavaScript SDK
|
|
4
|
+
*
|
|
5
|
+
* Provides schema and table creation for YAMO block persistence.
|
|
6
|
+
* YAMO blocks provide audit trail for all memory operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as arrow from "apache-arrow";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create YAMO blocks table schema
|
|
13
|
+
* Defines the structure for storing YAMO protocol blocks
|
|
14
|
+
*
|
|
15
|
+
* Schema includes:
|
|
16
|
+
* - Core identifiers (id, agent_id)
|
|
17
|
+
* - Operation tracking (operation_type, yamo_text)
|
|
18
|
+
* - Temporal data (timestamp)
|
|
19
|
+
* - Blockchain fields (block_hash, prev_hash) - nullable for future use
|
|
20
|
+
* - Metadata (JSON string for flexibility)
|
|
21
|
+
*
|
|
22
|
+
* @returns {import('apache-arrow').Schema} Arrow schema for YAMO blocks
|
|
23
|
+
*/
|
|
24
|
+
export function createYamoSchema() {
|
|
25
|
+
return new arrow.Schema([
|
|
26
|
+
// Core identifiers
|
|
27
|
+
new arrow.Field('id', new arrow.Utf8(), false),
|
|
28
|
+
new arrow.Field('agent_id', new arrow.Utf8(), true),
|
|
29
|
+
|
|
30
|
+
// Operation tracking
|
|
31
|
+
new arrow.Field('operation_type', new arrow.Utf8(), false), // 'retain', 'recall', 'reflect'
|
|
32
|
+
new arrow.Field('yamo_text', new arrow.Utf8(), false), // Full YAMO block content
|
|
33
|
+
|
|
34
|
+
// Temporal
|
|
35
|
+
new arrow.Field('timestamp', new arrow.Timestamp(arrow.TimeUnit.MILLISECOND), false),
|
|
36
|
+
|
|
37
|
+
// Blockchain fields (optional, nullable) - for future anchoring
|
|
38
|
+
new arrow.Field('block_hash', new arrow.Utf8(), true), // Hash of this block
|
|
39
|
+
new arrow.Field('prev_hash', new arrow.Utf8(), true), // Hash of previous block (for chain)
|
|
40
|
+
|
|
41
|
+
// Metadata (JSON string for flexibility)
|
|
42
|
+
new arrow.Field('metadata', new arrow.Utf8(), true), // Additional metadata as JSON
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create YAMO blocks table in LanceDB
|
|
48
|
+
* Creates the table if it doesn't exist, opens it if it does
|
|
49
|
+
*
|
|
50
|
+
* @param {import('@lancedb/lancedb').Connection} db - LanceDB connection
|
|
51
|
+
* @param {string} [tableName='yamo_blocks'] - Name of the table
|
|
52
|
+
* @returns {Promise<import('@lancedb/lancedb').Table>} The created or opened table
|
|
53
|
+
* @throws {Error} If table creation fails
|
|
54
|
+
*/
|
|
55
|
+
export async function createYamoTable(db, tableName = 'yamo_blocks') {
|
|
56
|
+
try {
|
|
57
|
+
// Check if table already exists
|
|
58
|
+
const existingTables = await db.tableNames();
|
|
59
|
+
|
|
60
|
+
if (existingTables.includes(tableName)) {
|
|
61
|
+
// Table exists, open it
|
|
62
|
+
return await db.openTable(tableName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create new table with YAMO schema
|
|
66
|
+
const schema = createYamoSchema();
|
|
67
|
+
const table = await db.createTable(tableName, [], { schema });
|
|
68
|
+
|
|
69
|
+
return table;
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(`Failed to create YAMO table '${tableName}': ${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a YAMO block record before insertion
|
|
79
|
+
* Checks for required fields and valid values
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} record - Record to validate
|
|
82
|
+
* @param {string} record.id - Block ID
|
|
83
|
+
* @param {string} record.operation_type - Operation type
|
|
84
|
+
* @param {string} record.yamo_text - YAMO block text
|
|
85
|
+
* @returns {Object} Validation result { valid, errors }
|
|
86
|
+
*/
|
|
87
|
+
export function validateYamoRecord(record) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
|
|
90
|
+
// Check required fields
|
|
91
|
+
if (!record.id) {
|
|
92
|
+
errors.push('Missing required field: id');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!record.operation_type) {
|
|
96
|
+
errors.push('Missing required field: operation_type');
|
|
97
|
+
} else {
|
|
98
|
+
// Validate operation_type is one of the allowed values
|
|
99
|
+
const validTypes = ['retain', 'recall', 'reflect'];
|
|
100
|
+
if (!validTypes.includes(record.operation_type)) {
|
|
101
|
+
errors.push(`Invalid operation_type: ${record.operation_type}. Must be one of: ${validTypes.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!record.yamo_text) {
|
|
106
|
+
errors.push('Missing required field: yamo_text');
|
|
107
|
+
} else {
|
|
108
|
+
// Validate YAMO block format
|
|
109
|
+
const requiredSections = ['agent:', 'intent:', 'context:', 'output:', 'log:'];
|
|
110
|
+
for (const section of requiredSections) {
|
|
111
|
+
if (!record.yamo_text.includes(section)) {
|
|
112
|
+
errors.push(`YAMO block missing required section: ${section}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
valid: errors.length === 0,
|
|
119
|
+
errors
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a YAMO block ID
|
|
125
|
+
* Creates a unique ID for a YAMO block
|
|
126
|
+
*
|
|
127
|
+
* @param {string} operationType - Type of operation
|
|
128
|
+
* @returns {string} Generated YAMO block ID
|
|
129
|
+
*/
|
|
130
|
+
export function generateYamoId(operationType) {
|
|
131
|
+
const timestamp = Date.now();
|
|
132
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
133
|
+
return `yamo_${operationType}_${timestamp}_${random}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if a table uses YAMO schema
|
|
138
|
+
* Detects if a table has the YAMO block schema structure
|
|
139
|
+
*
|
|
140
|
+
* @param {import('apache-arrow').Schema} schema - Table schema to check
|
|
141
|
+
* @returns {boolean} True if YAMO schema detected
|
|
142
|
+
*/
|
|
143
|
+
export function isYamoSchema(schema) {
|
|
144
|
+
// Check for unique YAMO fields
|
|
145
|
+
const hasYamoFields = schema.fields.some(f =>
|
|
146
|
+
f.name === 'operation_type' || f.name === 'yamo_text'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return hasYamoFields;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Export schema function as default for consistency with lancedb/schema.js
|
|
153
|
+
export default {
|
|
154
|
+
createYamoSchema,
|
|
155
|
+
createYamoTable,
|
|
156
|
+
validateYamoRecord,
|
|
157
|
+
generateYamoId,
|
|
158
|
+
isYamoSchema
|
|
159
|
+
};
|