server-memory-enhanced 2.2.1 → 2.3.1
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 +245 -44
- package/dist/index.js +197 -7
- package/dist/lib/constants.js +2 -1
- package/dist/lib/jsonl-storage-adapter.js +8 -4
- package/dist/lib/knowledge-graph-manager.js +237 -13
- package/dist/lib/save-memory-handler.js +48 -6
- package/dist/lib/schemas.js +46 -2
- package/dist/lib/validation.js +56 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,10 @@ An enhanced version of the Memory MCP server that provides persistent knowledge
|
|
|
15
15
|
### Entities
|
|
16
16
|
Each entity now includes:
|
|
17
17
|
- `name`: Entity identifier
|
|
18
|
-
- `entityType`: Type of entity
|
|
19
|
-
- `observations`: Array of
|
|
18
|
+
- `entityType`: Type of entity (free-form, any domain-specific type allowed)
|
|
19
|
+
- `observations`: Array of **versioned Observation objects** (not strings) - **BREAKING CHANGE**
|
|
20
|
+
- Each observation has: `id`, `content`, `timestamp`, `version`, `supersedes`, `superseded_by`
|
|
21
|
+
- Supports full version history tracking
|
|
20
22
|
- `agentThreadId`: Unique identifier for the agent thread
|
|
21
23
|
- `timestamp`: ISO 8601 timestamp of creation
|
|
22
24
|
- `confidence`: Confidence score (0.0 to 1.0)
|
|
@@ -52,39 +54,64 @@ The server stores data in separate JSONL files per agent thread:
|
|
|
52
54
|
|
|
53
55
|
## Available Tools
|
|
54
56
|
|
|
57
|
+
### ⭐ Recommended Tool (New)
|
|
58
|
+
1. **save_memory**: **[RECOMMENDED]** Unified tool for creating entities and relations atomically with server-side validation
|
|
59
|
+
- Enforces observation limits (max 300 chars, 3 sentences per observation, ignoring periods in version numbers)
|
|
60
|
+
- Requires at least 1 relation per entity (prevents orphaned nodes)
|
|
61
|
+
- Free-form entity types with soft normalization
|
|
62
|
+
- Atomic transactions (all-or-nothing)
|
|
63
|
+
- Bidirectional relation tracking
|
|
64
|
+
- Quality score calculation
|
|
65
|
+
- Clear, actionable error messages
|
|
66
|
+
|
|
55
67
|
### Core Operations
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
> ⚠️ **Note**: `create_entities` and `create_relations` are **deprecated**. New code should use `save_memory` for better reliability and validation.
|
|
69
|
+
|
|
70
|
+
1. **create_entities**: Create new entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
|
|
71
|
+
2. **create_relations**: Create relationships between entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
|
|
58
72
|
3. **add_observations**: Add observations to existing entities with metadata (including importance)
|
|
59
|
-
4. **
|
|
60
|
-
5. **
|
|
61
|
-
6. **
|
|
62
|
-
7. **
|
|
63
|
-
8. **
|
|
64
|
-
9. **
|
|
65
|
-
10. **
|
|
73
|
+
4. **update_observation**: **[NEW]** Update an existing observation by creating a new version with updated content, maintaining version history
|
|
74
|
+
5. **delete_entities**: Remove entities and cascading relations
|
|
75
|
+
6. **delete_observations**: Remove specific observations
|
|
76
|
+
7. **delete_relations**: Delete relationships
|
|
77
|
+
8. **read_graph**: Read the entire knowledge graph
|
|
78
|
+
9. **search_nodes**: Search entities by name, type, or observation content
|
|
79
|
+
10. **open_nodes**: Retrieve specific entities by name
|
|
80
|
+
11. **query_nodes**: Advanced querying with range-based filtering by timestamp, confidence, and importance
|
|
81
|
+
12. **list_entities**: List entities with optional filtering by type and name pattern for quick discovery
|
|
82
|
+
13. **validate_memory**: Validate entities without saving (dry-run) - check for errors before attempting save_memory
|
|
66
83
|
|
|
67
84
|
### Memory Management & Insights
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
14. **get_analytics**: **[NEW]** Get simple, LLM-friendly analytics about your knowledge graph
|
|
86
|
+
- Recent changes (last 10 entities)
|
|
87
|
+
- Top important entities (by importance score)
|
|
88
|
+
- Most connected entities (by relation count)
|
|
89
|
+
- Orphaned entities (quality check)
|
|
90
|
+
15. **get_observation_history**: **[NEW]** Retrieve version history for observations
|
|
91
|
+
- Track how observations evolve over time
|
|
92
|
+
- View complete version chains
|
|
93
|
+
- Supports rollback by viewing previous versions
|
|
94
|
+
16. **get_memory_stats**: Get comprehensive statistics (entity counts, thread activity, avg confidence/importance, recent activity)
|
|
95
|
+
17. **get_recent_changes**: Retrieve entities and relations created/modified since a specific timestamp
|
|
96
|
+
18. **prune_memory**: Remove old or low-importance entities to manage memory size
|
|
97
|
+
19. **bulk_update**: Efficiently update multiple entities at once (confidence, importance, observations)
|
|
98
|
+
20. **list_conversations**: List all available agent threads (conversations) with metadata including entity counts, relation counts, and activity timestamps
|
|
72
99
|
|
|
73
100
|
### Relationship Intelligence
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
21. **find_relation_path**: Find the shortest path of relationships between two entities (useful for "how are they connected?")
|
|
102
|
+
22. **get_context**: Retrieve entities and relations related to specified entities up to a certain depth
|
|
76
103
|
|
|
77
104
|
### Quality & Review
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
23. **detect_conflicts**: Detect potentially conflicting observations using pattern matching and negation detection
|
|
106
|
+
24. **flag_for_review**: Mark entities for human review with a specific reason (Human-in-the-Loop)
|
|
107
|
+
25. **get_flagged_entities**: Retrieve all entities flagged for review
|
|
81
108
|
|
|
82
109
|
## Usage
|
|
83
110
|
|
|
84
111
|
### Installation
|
|
85
112
|
|
|
86
113
|
```bash
|
|
87
|
-
npm install
|
|
114
|
+
npm install server-memory-enhanced
|
|
88
115
|
```
|
|
89
116
|
|
|
90
117
|
### Running the Server
|
|
@@ -95,44 +122,150 @@ npx mcp-server-memory-enhanced
|
|
|
95
122
|
|
|
96
123
|
### Configuration
|
|
97
124
|
|
|
125
|
+
#### File Storage (Default)
|
|
126
|
+
|
|
98
127
|
Set the `MEMORY_DIR_PATH` environment variable to customize the storage location:
|
|
99
128
|
|
|
100
129
|
```bash
|
|
101
130
|
MEMORY_DIR_PATH=/path/to/memory/directory npx mcp-server-memory-enhanced
|
|
102
131
|
```
|
|
103
132
|
|
|
104
|
-
|
|
133
|
+
#### Neo4j Storage (Optional)
|
|
134
|
+
|
|
135
|
+
The server supports Neo4j as an alternative storage backend. If Neo4j environment variables are set, the server will attempt to connect to Neo4j. If the connection fails or variables are not set, it will automatically fall back to file-based JSONL storage.
|
|
136
|
+
|
|
137
|
+
**Environment Variables:**
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Neo4j connection settings
|
|
141
|
+
export NEO4J_URI=neo4j://localhost:7687
|
|
142
|
+
export NEO4J_USERNAME=neo4j
|
|
143
|
+
export NEO4J_PASSWORD=your_password
|
|
144
|
+
export NEO4J_DATABASE=neo4j # Optional, defaults to 'neo4j'
|
|
145
|
+
|
|
146
|
+
# Run the server
|
|
147
|
+
npx mcp-server-memory-enhanced
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Using Docker Compose:**
|
|
151
|
+
|
|
152
|
+
A `docker-compose.yml` file is provided for local development with Neo4j:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Start Neo4j and the MCP server
|
|
156
|
+
docker-compose up
|
|
157
|
+
|
|
158
|
+
# The Neo4j browser will be available at http://localhost:7474
|
|
159
|
+
# Username: neo4j, Password: testpassword
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Using with Claude Desktop:**
|
|
163
|
+
|
|
164
|
+
Configure the server in your Claude Desktop configuration with Neo4j:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"mcpServers": {
|
|
169
|
+
"memory-enhanced": {
|
|
170
|
+
"command": "npx",
|
|
171
|
+
"args": ["-y", "mcp-server-memory-enhanced"],
|
|
172
|
+
"env": {
|
|
173
|
+
"NEO4J_URI": "neo4j://localhost:7687",
|
|
174
|
+
"NEO4J_USERNAME": "neo4j",
|
|
175
|
+
"NEO4J_PASSWORD": "your_password"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Benefits of Neo4j Storage:**
|
|
183
|
+
|
|
184
|
+
- **Graph-native queries**: Faster relationship traversals and path finding
|
|
185
|
+
- **Scalability**: Better performance with large knowledge graphs
|
|
186
|
+
- **Advanced queries**: Native support for graph algorithms
|
|
187
|
+
- **Visualization**: Use Neo4j Browser to visualize your knowledge graph
|
|
188
|
+
- **Automatic fallback**: If Neo4j is not available, automatically uses file storage
|
|
189
|
+
|
|
190
|
+
## User Guide
|
|
191
|
+
|
|
192
|
+
### ✨ Using save_memory (Recommended)
|
|
193
|
+
|
|
194
|
+
The `save_memory` tool is the recommended way to create entities and relations. It provides atomic transactions and server-side validation to ensure high-quality knowledge graphs.
|
|
195
|
+
|
|
196
|
+
#### Key Principles
|
|
197
|
+
|
|
198
|
+
1. **Atomic Observations**: Each observation should be a single, atomic fact
|
|
199
|
+
- ✅ Good: `"Works at Google"`, `"Lives in San Francisco"`
|
|
200
|
+
- ❌ Bad: `"Works at Google and lives in San Francisco and has a PhD in Computer Science"`
|
|
201
|
+
- **Max length**: 300 characters per observation
|
|
202
|
+
- **Max sentences**: 3 sentences per observation (technical content with version numbers supported)
|
|
203
|
+
|
|
204
|
+
2. **Mandatory Relations**: Every entity must connect to at least one other entity
|
|
205
|
+
- ✅ Good: `{ targetEntity: "Google", relationType: "works at" }`
|
|
206
|
+
- ❌ Bad: Empty relations array `[]`
|
|
207
|
+
- This prevents orphaned nodes and ensures a well-connected knowledge graph
|
|
208
|
+
|
|
209
|
+
3. **Free Entity Types**: Use any entity type that makes sense for your domain
|
|
210
|
+
- ✅ Good: `"Person"`, `"Company"`, `"Document"`, `"Recipe"`, `"Patient"`, `"API"`
|
|
211
|
+
- Soft normalization: `"person"` → `"Person"` (warning, not error)
|
|
212
|
+
- Space warning: `"API Key"` → suggests `"APIKey"`
|
|
213
|
+
|
|
214
|
+
4. **Error Messages**: The tool provides clear, actionable error messages
|
|
215
|
+
- Too long: `"Observation too long (350 chars). Max 300. Suggestion: Split into multiple observations."`
|
|
216
|
+
- No relations: `"Entity 'X' must have at least 1 relation. Suggestion: Add relations to show connections."`
|
|
217
|
+
- Too many sentences: `"Too many sentences (4). Max 3. Suggestion: Split this into 4 separate observations."`
|
|
218
|
+
|
|
219
|
+
### Example Usage
|
|
105
220
|
|
|
106
221
|
```typescript
|
|
107
|
-
//
|
|
108
|
-
await
|
|
222
|
+
// ✅ RECOMMENDED: Use save_memory for atomic entity and relation creation
|
|
223
|
+
await save_memory({
|
|
109
224
|
entities: [
|
|
110
225
|
{
|
|
111
226
|
name: "Alice",
|
|
112
|
-
entityType: "
|
|
113
|
-
observations: ["
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
227
|
+
entityType: "Person",
|
|
228
|
+
observations: ["Works at Google", "Lives in SF"], // Atomic facts, under 300 chars
|
|
229
|
+
relations: [{ targetEntity: "Bob", relationType: "knows" }] // At least 1 relation required
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "Bob",
|
|
233
|
+
entityType: "Person",
|
|
234
|
+
observations: ["Works at Microsoft"],
|
|
235
|
+
relations: [{ targetEntity: "Alice", relationType: "knows" }]
|
|
118
236
|
}
|
|
119
|
-
]
|
|
237
|
+
],
|
|
238
|
+
threadId: "conversation-001"
|
|
120
239
|
});
|
|
121
240
|
|
|
122
|
-
//
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
241
|
+
// Get analytics about your knowledge graph
|
|
242
|
+
await get_analytics({
|
|
243
|
+
threadId: "conversation-001"
|
|
244
|
+
});
|
|
245
|
+
// Returns: {
|
|
246
|
+
// recent_changes: [...], // Last 10 entities
|
|
247
|
+
// top_important: [...], // Top 10 by importance
|
|
248
|
+
// most_connected: [...], // Top 10 by relation count
|
|
249
|
+
// orphaned_entities: [...] // Quality check
|
|
250
|
+
// }
|
|
251
|
+
|
|
252
|
+
// Get observation version history
|
|
253
|
+
await get_observation_history({
|
|
254
|
+
entityName: "Python Scripts",
|
|
255
|
+
observationId: "obs_abc123"
|
|
135
256
|
});
|
|
257
|
+
// Returns: { history: [{ id, content, version, timestamp, supersedes, superseded_by }, ...] }
|
|
258
|
+
|
|
259
|
+
// Update an existing observation (creates a new version)
|
|
260
|
+
await update_observation({
|
|
261
|
+
entityName: "Alice",
|
|
262
|
+
observationId: "obs_abc123",
|
|
263
|
+
newContent: "Works at Google (Senior Engineer)",
|
|
264
|
+
agentThreadId: "conversation-001",
|
|
265
|
+
timestamp: "2024-01-20T12:00:00Z",
|
|
266
|
+
confidence: 0.95 // Optional: update confidence
|
|
267
|
+
});
|
|
268
|
+
// Returns: { success: true, updatedObservation: { id, content, version: 2, supersedes, ... }, message: "..." }
|
|
136
269
|
|
|
137
270
|
// Query nodes with range-based filtering
|
|
138
271
|
await queryNodes({
|
|
@@ -146,6 +279,10 @@ await queryNodes({
|
|
|
146
279
|
await getMemoryStats();
|
|
147
280
|
// Returns: { entityCount, relationCount, threadCount, entityTypes, avgConfidence, avgImportance, recentActivity }
|
|
148
281
|
|
|
282
|
+
// List all conversations (agent threads)
|
|
283
|
+
await listConversations();
|
|
284
|
+
// Returns: { conversations: [{ agentThreadId, entityCount, relationCount, firstCreated, lastUpdated }, ...] }
|
|
285
|
+
|
|
149
286
|
// Get recent changes since last interaction
|
|
150
287
|
await getRecentChanges({ since: "2024-01-20T10:00:00Z" });
|
|
151
288
|
|
|
@@ -173,6 +310,49 @@ await bulkUpdate({
|
|
|
173
310
|
await pruneMemory({ olderThan: "2024-01-01T00:00:00Z", importanceLessThan: 0.3, keepMinEntities: 100 });
|
|
174
311
|
```
|
|
175
312
|
|
|
313
|
+
### 🔄 Migration Guide
|
|
314
|
+
|
|
315
|
+
For users of the old `create_entities` and `create_relations` tools:
|
|
316
|
+
|
|
317
|
+
#### What Changed
|
|
318
|
+
- **Old approach**: Two separate tools that could be used independently
|
|
319
|
+
- `create_entities` → creates entities
|
|
320
|
+
- `create_relations` → creates relations (optional, often skipped by LLMs)
|
|
321
|
+
- **New approach**: Single `save_memory` tool with atomic transactions
|
|
322
|
+
- Creates entities and relations together
|
|
323
|
+
- Enforces mandatory relations (at least 1 per entity)
|
|
324
|
+
- Validates observation length and atomicity
|
|
325
|
+
|
|
326
|
+
#### Migrating Your Code
|
|
327
|
+
```typescript
|
|
328
|
+
// ❌ OLD WAY (deprecated but still works)
|
|
329
|
+
await create_entities({
|
|
330
|
+
entities: [{ name: "Alice", entityType: "person", observations: ["works at Google and lives in SF"] }]
|
|
331
|
+
});
|
|
332
|
+
await create_relations({ // Often forgotten!
|
|
333
|
+
relations: [{ from: "Alice", to: "Bob", relationType: "knows" }]
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ✅ NEW WAY (recommended)
|
|
337
|
+
await save_memory({
|
|
338
|
+
entities: [
|
|
339
|
+
{
|
|
340
|
+
name: "Alice",
|
|
341
|
+
entityType: "Person",
|
|
342
|
+
observations: ["Works at Google", "Lives in SF"], // Split into atomic facts
|
|
343
|
+
relations: [{ targetEntity: "Bob", relationType: "knows" }] // Required!
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
threadId: "conversation-001"
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### Migration Strategy
|
|
351
|
+
1. **Old tools remain available**: `create_entities` and `create_relations` are deprecated but not removed
|
|
352
|
+
2. **No forced migration**: Update your code gradually at your own pace
|
|
353
|
+
3. **New code should use `save_memory`**: Benefits from validation and atomic transactions
|
|
354
|
+
4. **Observation versioning**: New installations use versioned observations (breaking change for data model)
|
|
355
|
+
|
|
176
356
|
## Development
|
|
177
357
|
|
|
178
358
|
### Build
|
|
@@ -196,3 +376,24 @@ npm run watch
|
|
|
196
376
|
## License
|
|
197
377
|
|
|
198
378
|
MIT
|
|
379
|
+
|
|
380
|
+
## 🤝 Contributing
|
|
381
|
+
|
|
382
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
383
|
+
|
|
384
|
+
## 🔒 Security
|
|
385
|
+
|
|
386
|
+
See [SECURITY.MD](SECURITY.md) for reporting security vulnerabilities.
|
|
387
|
+
|
|
388
|
+
## 📜 License
|
|
389
|
+
|
|
390
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
391
|
+
|
|
392
|
+
## 💬 Community
|
|
393
|
+
|
|
394
|
+
- [GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
|
|
395
|
+
- [Model Context Protocol Documentation](https://modelcontextprotocol.io)
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
Part of the [Model Context Protocol](https://modelcontextprotocol.io) project.
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import { promises as fs } from 'fs';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
|
|
9
|
-
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema } from './lib/schemas.js';
|
|
9
|
+
import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } from './lib/schemas.js';
|
|
10
10
|
import { handleSaveMemory } from './lib/save-memory-handler.js';
|
|
11
|
+
import { validateSaveMemoryRequest } from './lib/validation.js';
|
|
11
12
|
import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
|
|
12
13
|
import { Neo4jStorageAdapter } from './lib/neo4j-storage-adapter.js';
|
|
13
14
|
import { NEO4J_ENV_VARS, STORAGE_LOG_MESSAGES, NEO4J_ERROR_MESSAGES } from './lib/storage-config.js';
|
|
@@ -110,27 +111,57 @@ const server = new McpServer({
|
|
|
110
111
|
// Register NEW save_memory tool (Section 1 of spec - Unified Tool)
|
|
111
112
|
server.registerTool("save_memory", {
|
|
112
113
|
title: "Save Memory",
|
|
113
|
-
description: "Save entities and their relations to memory graph atomically. RULES: 1) Each observation max
|
|
114
|
+
description: "Save entities and their relations to memory graph atomically. RULES: 1) Each observation max 300 chars (atomic facts, technical content supported). 2) Each entity MUST have at least 1 relation. This is the recommended way to create entities and relations.",
|
|
114
115
|
inputSchema: SaveMemoryInputSchema,
|
|
115
116
|
outputSchema: SaveMemoryOutputSchema
|
|
116
117
|
}, async (input) => {
|
|
117
|
-
const result = await handleSaveMemory(input, (entities) => knowledgeGraphManager.createEntities(entities), (relations) => knowledgeGraphManager.createRelations(relations));
|
|
118
|
+
const result = await handleSaveMemory(input, (entities) => knowledgeGraphManager.createEntities(entities), (relations) => knowledgeGraphManager.createRelations(relations), (threadId) => knowledgeGraphManager.getEntityNamesInThread(threadId));
|
|
118
119
|
if (result.success) {
|
|
120
|
+
// Build success message with entity names
|
|
121
|
+
let successText = `✓ Successfully saved ${result.created.entities} entities and ${result.created.relations} relations.\n` +
|
|
122
|
+
`Quality score: ${(result.quality_score * 100).toFixed(1)}%\n`;
|
|
123
|
+
if (result.created.entity_names && result.created.entity_names.length > 0) {
|
|
124
|
+
successText += `\nCreated entities: ${result.created.entity_names.join(', ')}\n`;
|
|
125
|
+
}
|
|
126
|
+
if (result.warnings.length > 0) {
|
|
127
|
+
successText += `\nWarnings:\n${result.warnings.join('\n')}`;
|
|
128
|
+
}
|
|
119
129
|
return {
|
|
120
130
|
content: [{
|
|
121
131
|
type: "text",
|
|
122
|
-
text:
|
|
123
|
-
`Quality score: ${(result.quality_score * 100).toFixed(1)}%\n` +
|
|
124
|
-
(result.warnings.length > 0 ? `\nWarnings:\n${result.warnings.join('\n')}` : '')
|
|
132
|
+
text: successText
|
|
125
133
|
}],
|
|
126
134
|
structuredContent: result
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
else {
|
|
138
|
+
// Format validation errors for display
|
|
139
|
+
let errorText = '✗ Validation failed:\n\n';
|
|
140
|
+
if (result.validation_errors) {
|
|
141
|
+
if (Array.isArray(result.validation_errors) && result.validation_errors.length > 0) {
|
|
142
|
+
// Check if structured errors
|
|
143
|
+
if (typeof result.validation_errors[0] === 'object') {
|
|
144
|
+
const structuredErrors = result.validation_errors;
|
|
145
|
+
errorText += structuredErrors.map(err => {
|
|
146
|
+
let msg = `Entity #${err.entity_index} "${err.entity_name}" (${err.entity_type}):\n`;
|
|
147
|
+
err.errors.forEach((e) => msg += ` - ${e}\n`);
|
|
148
|
+
if (err.observations && err.observations.length > 0) {
|
|
149
|
+
msg += ` Observations: ${err.observations.join(', ')}\n`;
|
|
150
|
+
}
|
|
151
|
+
return msg;
|
|
152
|
+
}).join('\n');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Fallback to string errors
|
|
156
|
+
errorText += result.validation_errors.join('\n');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
errorText += '\nFix all validation errors and retry. All entities must be valid to maintain memory integrity.';
|
|
130
161
|
return {
|
|
131
162
|
content: [{
|
|
132
163
|
type: "text",
|
|
133
|
-
text:
|
|
164
|
+
text: errorText
|
|
134
165
|
}],
|
|
135
166
|
structuredContent: result,
|
|
136
167
|
isError: true
|
|
@@ -236,6 +267,32 @@ server.registerTool("delete_observations", {
|
|
|
236
267
|
structuredContent: { success: true, message: "Observations deleted successfully" }
|
|
237
268
|
};
|
|
238
269
|
});
|
|
270
|
+
// Register update_observation tool
|
|
271
|
+
server.registerTool("update_observation", {
|
|
272
|
+
title: "Update Observation",
|
|
273
|
+
description: "Update an existing observation by creating a new version with updated content. This maintains version history through the supersedes/superseded_by chain.",
|
|
274
|
+
inputSchema: UpdateObservationInputSchema.shape,
|
|
275
|
+
outputSchema: UpdateObservationOutputSchema.shape
|
|
276
|
+
}, async ({ entityName, observationId, newContent, agentThreadId, timestamp, confidence, importance }) => {
|
|
277
|
+
const updatedObservation = await knowledgeGraphManager.updateObservation({
|
|
278
|
+
entityName,
|
|
279
|
+
observationId,
|
|
280
|
+
newContent,
|
|
281
|
+
agentThreadId,
|
|
282
|
+
timestamp,
|
|
283
|
+
confidence,
|
|
284
|
+
importance
|
|
285
|
+
});
|
|
286
|
+
const message = `Observation updated successfully. New version: ${updatedObservation.id} (v${updatedObservation.version})`;
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: message }],
|
|
289
|
+
structuredContent: {
|
|
290
|
+
success: true,
|
|
291
|
+
updatedObservation,
|
|
292
|
+
message
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
});
|
|
239
296
|
// Register delete_relations tool
|
|
240
297
|
server.registerTool("delete_relations", {
|
|
241
298
|
title: "Delete Relations",
|
|
@@ -329,6 +386,139 @@ server.registerTool("query_nodes", {
|
|
|
329
386
|
structuredContent: { ...graph }
|
|
330
387
|
};
|
|
331
388
|
});
|
|
389
|
+
// Register list_entities tool for simple entity lookup
|
|
390
|
+
server.registerTool("list_entities", {
|
|
391
|
+
title: "List Entities",
|
|
392
|
+
description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery.",
|
|
393
|
+
inputSchema: ListEntitiesInputSchema,
|
|
394
|
+
outputSchema: ListEntitiesOutputSchema
|
|
395
|
+
}, async (input) => {
|
|
396
|
+
const { threadId, entityType, namePattern } = input;
|
|
397
|
+
const entities = await knowledgeGraphManager.listEntities(threadId, entityType, namePattern);
|
|
398
|
+
return {
|
|
399
|
+
content: [{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: `Found ${entities.length} entities:\n` +
|
|
402
|
+
entities.map(e => ` - ${e.name} (${e.entityType})`).join('\n')
|
|
403
|
+
}],
|
|
404
|
+
structuredContent: { entities }
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
// Register validate_memory tool for pre-validation (dry-run)
|
|
408
|
+
server.registerTool("validate_memory", {
|
|
409
|
+
title: "Validate Memory",
|
|
410
|
+
description: "Validate entities without saving (dry-run). Check for errors before attempting save_memory. Returns detailed validation results per entity.",
|
|
411
|
+
inputSchema: ValidateMemoryInputSchema,
|
|
412
|
+
outputSchema: ValidateMemoryOutputSchema
|
|
413
|
+
}, async (input) => {
|
|
414
|
+
const { entities, threadId } = input;
|
|
415
|
+
// Get existing entity names for cross-thread reference validation
|
|
416
|
+
let existingEntityNames;
|
|
417
|
+
try {
|
|
418
|
+
existingEntityNames = await knowledgeGraphManager.getEntityNamesInThread(threadId);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// If we can't get existing entities, proceed without cross-thread validation
|
|
422
|
+
}
|
|
423
|
+
// Preserve original entityType values before validation normalizes them
|
|
424
|
+
// This is needed to match warnings to the correct entities
|
|
425
|
+
const originalEntityTypes = entities.map((e) => e.entityType);
|
|
426
|
+
// Run validation (same logic as save_memory but without saving)
|
|
427
|
+
const validationResult = validateSaveMemoryRequest(entities, existingEntityNames);
|
|
428
|
+
// Transform validation result into per-entity format
|
|
429
|
+
const results = new Map();
|
|
430
|
+
// Initialize all entities as valid
|
|
431
|
+
entities.forEach((entity, index) => {
|
|
432
|
+
results.set(index, {
|
|
433
|
+
index: index,
|
|
434
|
+
name: entity.name,
|
|
435
|
+
type: originalEntityTypes[index], // Use original type, not normalized
|
|
436
|
+
valid: true,
|
|
437
|
+
errors: [],
|
|
438
|
+
warnings: []
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
// Add errors to corresponding entities
|
|
442
|
+
validationResult.errors.forEach((err) => {
|
|
443
|
+
const result = results.get(err.entityIndex);
|
|
444
|
+
if (result) {
|
|
445
|
+
result.valid = false;
|
|
446
|
+
const errorMsg = err.suggestion
|
|
447
|
+
? `${err.error} Suggestion: ${err.suggestion}`
|
|
448
|
+
: err.error;
|
|
449
|
+
result.errors.push(errorMsg);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
// Add warnings to corresponding entities
|
|
453
|
+
validationResult.warnings.forEach((warning) => {
|
|
454
|
+
// Parse entity type from warning if possible, otherwise add to first entity
|
|
455
|
+
const entityMatch = warning.match(/EntityType '([^']+)'/);
|
|
456
|
+
if (entityMatch) {
|
|
457
|
+
const warningEntityType = entityMatch[1];
|
|
458
|
+
let attached = false;
|
|
459
|
+
// Find entity whose ORIGINAL entityType matches the type in the warning
|
|
460
|
+
// (warnings contain the original type before normalization)
|
|
461
|
+
originalEntityTypes.forEach((origType, index) => {
|
|
462
|
+
if (origType === warningEntityType) {
|
|
463
|
+
const result = results.get(index);
|
|
464
|
+
if (result) {
|
|
465
|
+
result.warnings.push(warning);
|
|
466
|
+
attached = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// If no matching entity type found, attach to first entity as fallback
|
|
471
|
+
if (!attached) {
|
|
472
|
+
const firstResult = results.get(0);
|
|
473
|
+
if (firstResult) {
|
|
474
|
+
firstResult.warnings.push(warning);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// No entity type information in warning; attach to first entity
|
|
480
|
+
const firstResult = results.get(0);
|
|
481
|
+
if (firstResult) {
|
|
482
|
+
firstResult.warnings.push(warning);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
const resultArray = Array.from(results.values());
|
|
487
|
+
const allValid = resultArray.every(r => r.valid);
|
|
488
|
+
// Format response text
|
|
489
|
+
let responseText = allValid
|
|
490
|
+
? `✓ All ${entities.length} entities are valid and ready to save.\n`
|
|
491
|
+
: `✗ Validation failed for ${resultArray.filter(r => !r.valid).length} of ${entities.length} entities:\n\n`;
|
|
492
|
+
if (!allValid) {
|
|
493
|
+
resultArray.filter(r => !r.valid).forEach(r => {
|
|
494
|
+
responseText += `Entity #${r.index} "${r.name}" (${r.type}):\n`;
|
|
495
|
+
r.errors.forEach(e => responseText += ` - ${e}\n`);
|
|
496
|
+
if (r.warnings.length > 0) {
|
|
497
|
+
r.warnings.forEach(w => responseText += ` ⚠ ${w}\n`);
|
|
498
|
+
}
|
|
499
|
+
responseText += '\n';
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
// Add warnings for valid entities if any
|
|
503
|
+
const validWithWarnings = resultArray.filter(r => r.valid && r.warnings.length > 0);
|
|
504
|
+
if (validWithWarnings.length > 0) {
|
|
505
|
+
responseText += 'Warnings:\n';
|
|
506
|
+
validWithWarnings.forEach(r => {
|
|
507
|
+
responseText += `Entity #${r.index} "${r.name}" (${r.type}):\n`;
|
|
508
|
+
r.warnings.forEach(w => responseText += ` ⚠ ${w}\n`);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
content: [{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: responseText
|
|
515
|
+
}],
|
|
516
|
+
structuredContent: {
|
|
517
|
+
all_valid: allValid,
|
|
518
|
+
results: resultArray
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
});
|
|
332
522
|
// Register get_memory_stats tool
|
|
333
523
|
server.registerTool("get_memory_stats", {
|
|
334
524
|
title: "Get Memory Statistics",
|
package/dist/lib/constants.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* Maximum length for observations in characters
|
|
7
7
|
* Per spec Section 2: Hard Limits on Observation Length
|
|
8
|
+
* Increased to 300 to accommodate technical content (URLs, connection strings, etc.)
|
|
8
9
|
*/
|
|
9
|
-
export const MAX_OBSERVATION_LENGTH =
|
|
10
|
+
export const MAX_OBSERVATION_LENGTH = 300;
|
|
10
11
|
/**
|
|
11
12
|
* Maximum number of sentences allowed per observation
|
|
12
13
|
* Per spec Section 2: Hard Limits on Observation Length
|
|
@@ -240,10 +240,14 @@ export class JsonlStorageAdapter {
|
|
|
240
240
|
* Serialize thread data to JSONL lines
|
|
241
241
|
*/
|
|
242
242
|
serializeThreadData(threadData) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
const lines = [];
|
|
244
|
+
for (const e of threadData.entities) {
|
|
245
|
+
lines.push(this.serializeEntity(e));
|
|
246
|
+
}
|
|
247
|
+
for (const r of threadData.relations) {
|
|
248
|
+
lines.push(this.serializeRelation(r));
|
|
249
|
+
}
|
|
250
|
+
return lines;
|
|
247
251
|
}
|
|
248
252
|
/**
|
|
249
253
|
* Save data for a specific thread
|
|
@@ -11,6 +11,38 @@ export class KnowledgeGraphManager {
|
|
|
11
11
|
this.storage = storageAdapter || new JsonlStorageAdapter(memoryDirPath);
|
|
12
12
|
// Lazy initialization - will be called on first operation
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if content contains any negation words (using word boundary matching)
|
|
16
|
+
* Handles punctuation and contractions, avoids creating intermediate Set for performance
|
|
17
|
+
*/
|
|
18
|
+
hasNegation(content) {
|
|
19
|
+
// Extract words using word boundary regex, preserving contractions (include apostrophes)
|
|
20
|
+
const lowerContent = content.toLowerCase();
|
|
21
|
+
const wordMatches = lowerContent.match(/\b[\w']+\b/g);
|
|
22
|
+
if (!wordMatches) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// Check each word against negation words without creating intermediate Set
|
|
26
|
+
for (const word of wordMatches) {
|
|
27
|
+
if (KnowledgeGraphManager.NEGATION_WORDS.has(word)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a composite key for relation deduplication.
|
|
35
|
+
*
|
|
36
|
+
* We explicitly normalize the components to primitive strings to ensure
|
|
37
|
+
* stable serialization and to document the assumption that `from`, `to`,
|
|
38
|
+
* and `relationType` are simple string identifiers.
|
|
39
|
+
*/
|
|
40
|
+
createRelationKey(relation) {
|
|
41
|
+
const from = String(relation.from);
|
|
42
|
+
const to = String(relation.to);
|
|
43
|
+
const relationType = String(relation.relationType);
|
|
44
|
+
return JSON.stringify([from, to, relationType]);
|
|
45
|
+
}
|
|
14
46
|
/**
|
|
15
47
|
* Ensure storage is initialized before any operation
|
|
16
48
|
* This is called automatically by all public methods
|
|
@@ -21,12 +53,80 @@ export class KnowledgeGraphManager {
|
|
|
21
53
|
}
|
|
22
54
|
await this.initializePromise;
|
|
23
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Find an entity by name in the knowledge graph.
|
|
58
|
+
* @param graph - The knowledge graph to search
|
|
59
|
+
* @param entityName - Name of the entity to find
|
|
60
|
+
* @returns The found entity
|
|
61
|
+
* @throws Error if entity not found
|
|
62
|
+
*/
|
|
63
|
+
findEntity(graph, entityName) {
|
|
64
|
+
const entity = graph.entities.find(e => e.name === entityName);
|
|
65
|
+
if (!entity) {
|
|
66
|
+
throw new Error(`Entity '${entityName}' not found`);
|
|
67
|
+
}
|
|
68
|
+
return entity;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Find an observation by ID within an entity.
|
|
72
|
+
* @param entity - The entity containing the observation
|
|
73
|
+
* @param observationId - ID of the observation to find
|
|
74
|
+
* @returns The found observation
|
|
75
|
+
* @throws Error if observation not found
|
|
76
|
+
*/
|
|
77
|
+
findObservation(entity, observationId) {
|
|
78
|
+
const observation = entity.observations.find(o => o.id === observationId);
|
|
79
|
+
if (!observation) {
|
|
80
|
+
throw new Error(`Observation '${observationId}' not found in entity '${entity.name}'`);
|
|
81
|
+
}
|
|
82
|
+
return observation;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate that an observation can be updated (not already superseded).
|
|
86
|
+
* @param observation - The observation to validate
|
|
87
|
+
* @throws Error if observation has already been superseded
|
|
88
|
+
*/
|
|
89
|
+
validateObservationNotSuperseded(observation) {
|
|
90
|
+
if (observation.superseded_by) {
|
|
91
|
+
throw new Error(`Observation '${observation.id}' has already been superseded by '${observation.superseded_by}'. Update the latest version instead.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve confidence value using inheritance chain: params > observation > entity.
|
|
96
|
+
* @param providedValue - Value provided in parameters (optional)
|
|
97
|
+
* @param observationValue - Value from observation (optional)
|
|
98
|
+
* @param entityValue - Value from entity (fallback)
|
|
99
|
+
* @returns Resolved confidence value
|
|
100
|
+
*/
|
|
101
|
+
resolveInheritedValue(providedValue, observationValue, entityValue) {
|
|
102
|
+
return providedValue ?? observationValue ?? entityValue;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a new observation version from an existing observation.
|
|
106
|
+
* @param oldObs - The observation being updated
|
|
107
|
+
* @param entity - The entity containing the observation
|
|
108
|
+
* @param params - Update parameters
|
|
109
|
+
* @returns New observation with incremented version
|
|
110
|
+
*/
|
|
111
|
+
createObservationVersion(oldObs, entity, params) {
|
|
112
|
+
return {
|
|
113
|
+
id: `obs_${randomUUID()}`,
|
|
114
|
+
content: params.newContent,
|
|
115
|
+
timestamp: params.timestamp,
|
|
116
|
+
version: oldObs.version + 1,
|
|
117
|
+
supersedes: oldObs.id,
|
|
118
|
+
agentThreadId: params.agentThreadId,
|
|
119
|
+
confidence: this.resolveInheritedValue(params.confidence, oldObs.confidence, entity.confidence),
|
|
120
|
+
importance: this.resolveInheritedValue(params.importance, oldObs.importance, entity.importance)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
24
123
|
async createEntities(entities) {
|
|
25
124
|
await this.ensureInitialized();
|
|
26
125
|
const graph = await this.storage.loadGraph();
|
|
27
126
|
// Entity names are globally unique across all threads in the collaborative knowledge graph
|
|
28
127
|
// This prevents duplicate entities while allowing multiple threads to contribute to the same entity
|
|
29
|
-
const
|
|
128
|
+
const existingNames = new Set(graph.entities.map(e => e.name));
|
|
129
|
+
const newEntities = entities.filter(e => !existingNames.has(e.name));
|
|
30
130
|
graph.entities.push(...newEntities);
|
|
31
131
|
await this.storage.saveGraph(graph);
|
|
32
132
|
return newEntities;
|
|
@@ -44,9 +144,15 @@ export class KnowledgeGraphManager {
|
|
|
44
144
|
});
|
|
45
145
|
// Relations are globally unique by (from, to, relationType) across all threads
|
|
46
146
|
// This enables multiple threads to collaboratively build the knowledge graph
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
147
|
+
const existingRelationKeys = new Set(graph.relations.map(r => this.createRelationKey(r)));
|
|
148
|
+
// Create composite keys once per valid relation to avoid duplicate serialization
|
|
149
|
+
const validRelationsWithKeys = validRelations.map(r => ({
|
|
150
|
+
relation: r,
|
|
151
|
+
key: this.createRelationKey(r)
|
|
152
|
+
}));
|
|
153
|
+
const newRelations = validRelationsWithKeys
|
|
154
|
+
.filter(item => !existingRelationKeys.has(item.key))
|
|
155
|
+
.map(item => item.relation);
|
|
50
156
|
graph.relations.push(...newRelations);
|
|
51
157
|
await this.storage.saveGraph(graph);
|
|
52
158
|
return newRelations;
|
|
@@ -60,10 +166,16 @@ export class KnowledgeGraphManager {
|
|
|
60
166
|
}
|
|
61
167
|
// Check for existing observations with same content to create version chain
|
|
62
168
|
const newObservations = [];
|
|
169
|
+
// Build a Set of existing observation contents for efficient lookup (single-pass)
|
|
170
|
+
const existingContents = entity.observations.reduce((set, obs) => {
|
|
171
|
+
if (!obs.superseded_by) {
|
|
172
|
+
set.add(obs.content);
|
|
173
|
+
}
|
|
174
|
+
return set;
|
|
175
|
+
}, new Set());
|
|
63
176
|
for (const content of o.contents) {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
if (existingObs) {
|
|
177
|
+
// Check if observation with this content already exists (latest version)
|
|
178
|
+
if (existingContents.has(content)) {
|
|
67
179
|
// Don't add duplicate - observation with this content already exists
|
|
68
180
|
// Versioning is for UPDATES to content, not for re-asserting the same content
|
|
69
181
|
continue;
|
|
@@ -92,8 +204,9 @@ export class KnowledgeGraphManager {
|
|
|
92
204
|
}
|
|
93
205
|
async deleteEntities(entityNames) {
|
|
94
206
|
const graph = await this.storage.loadGraph();
|
|
95
|
-
|
|
96
|
-
graph.
|
|
207
|
+
const namesToDelete = new Set(entityNames);
|
|
208
|
+
graph.entities = graph.entities.filter(e => !namesToDelete.has(e.name));
|
|
209
|
+
graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
|
|
97
210
|
await this.storage.saveGraph(graph);
|
|
98
211
|
}
|
|
99
212
|
async deleteObservations(deletions) {
|
|
@@ -107,6 +220,41 @@ export class KnowledgeGraphManager {
|
|
|
107
220
|
});
|
|
108
221
|
await this.storage.saveGraph(graph);
|
|
109
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Update an existing observation by creating a new version with updated content.
|
|
225
|
+
* This maintains the version history through the supersedes/superseded_by chain.
|
|
226
|
+
*
|
|
227
|
+
* @param params - Update parameters
|
|
228
|
+
* @param params.entityName - Name of the entity containing the observation
|
|
229
|
+
* @param params.observationId - ID of the observation to update
|
|
230
|
+
* @param params.newContent - New content for the observation
|
|
231
|
+
* @param params.agentThreadId - Agent thread ID making this update
|
|
232
|
+
* @param params.timestamp - ISO 8601 timestamp of the update
|
|
233
|
+
* @param params.confidence - Optional confidence score (0-1), inherits from old observation if not provided
|
|
234
|
+
* @param params.importance - Optional importance score (0-1), inherits from old observation if not provided
|
|
235
|
+
* @returns The newly created observation with incremented version number
|
|
236
|
+
* @throws Error if entity not found
|
|
237
|
+
* @throws Error if observation not found
|
|
238
|
+
* @throws Error if observation has already been superseded (must update latest version)
|
|
239
|
+
*/
|
|
240
|
+
async updateObservation(params) {
|
|
241
|
+
await this.ensureInitialized();
|
|
242
|
+
const graph = await this.storage.loadGraph();
|
|
243
|
+
// Find and validate the entity and observation
|
|
244
|
+
const entity = this.findEntity(graph, params.entityName);
|
|
245
|
+
const oldObs = this.findObservation(entity, params.observationId);
|
|
246
|
+
this.validateObservationNotSuperseded(oldObs);
|
|
247
|
+
// Create new version with inheritance chain
|
|
248
|
+
const newObs = this.createObservationVersion(oldObs, entity, params);
|
|
249
|
+
// Link old observation to new one
|
|
250
|
+
oldObs.superseded_by = newObs.id;
|
|
251
|
+
// Add new observation to entity
|
|
252
|
+
entity.observations.push(newObs);
|
|
253
|
+
// Update entity timestamp
|
|
254
|
+
entity.timestamp = params.timestamp;
|
|
255
|
+
await this.storage.saveGraph(graph);
|
|
256
|
+
return newObs;
|
|
257
|
+
}
|
|
110
258
|
async deleteRelations(relations) {
|
|
111
259
|
const graph = await this.storage.loadGraph();
|
|
112
260
|
// Delete relations globally across all threads by matching (from, to, relationType)
|
|
@@ -203,6 +351,69 @@ export class KnowledgeGraphManager {
|
|
|
203
351
|
relations: filteredRelations,
|
|
204
352
|
};
|
|
205
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Get names of all entities that can be referenced in relations.
|
|
356
|
+
* @returns Set of entity names that exist in the graph.
|
|
357
|
+
*
|
|
358
|
+
* Note: Returns ALL entities globally because entity names are globally unique across
|
|
359
|
+
* all threads in the collaborative knowledge graph (by design - see createEntities).
|
|
360
|
+
* This enables any thread to reference any existing entity, supporting incremental
|
|
361
|
+
* building and cross-thread collaboration. Thread-specific filtering is not needed
|
|
362
|
+
* since entity names cannot conflict across threads.
|
|
363
|
+
*/
|
|
364
|
+
async getAllEntityNames() {
|
|
365
|
+
const graph = await this.storage.loadGraph();
|
|
366
|
+
const entityNames = new Set();
|
|
367
|
+
// Return all entities in the graph that can be referenced
|
|
368
|
+
// This allows incremental building: entities from previous save_memory calls
|
|
369
|
+
// can be referenced in new calls, enabling cross-save entity relations
|
|
370
|
+
for (const entity of graph.entities) {
|
|
371
|
+
entityNames.add(entity.name);
|
|
372
|
+
}
|
|
373
|
+
return entityNames;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* @deprecated Use {@link getAllEntityNames} instead.
|
|
377
|
+
*
|
|
378
|
+
* This method is kept for backward compatibility. It accepts a threadId parameter
|
|
379
|
+
* for API consistency but does not use it for filtering; it returns the same
|
|
380
|
+
* global set of entity names as {@link getAllEntityNames}.
|
|
381
|
+
*
|
|
382
|
+
* @param threadId The thread ID (accepted but not used)
|
|
383
|
+
* @returns Set of entity names that exist in the graph
|
|
384
|
+
*/
|
|
385
|
+
async getEntityNamesInThread(threadId) {
|
|
386
|
+
return this.getAllEntityNames();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* List entities with optional filtering by type and name pattern
|
|
390
|
+
* @param threadId Optional thread ID to filter by. If not provided, returns entities from all threads.
|
|
391
|
+
* @param entityType Optional entity type filter (exact match)
|
|
392
|
+
* @param namePattern Optional name pattern filter (case-insensitive substring match)
|
|
393
|
+
* @returns Array of entities with name and entityType
|
|
394
|
+
*/
|
|
395
|
+
async listEntities(threadId, entityType, namePattern) {
|
|
396
|
+
const graph = await this.storage.loadGraph();
|
|
397
|
+
let filteredEntities = graph.entities;
|
|
398
|
+
// Filter by thread ID if specified (otherwise returns all threads)
|
|
399
|
+
if (threadId) {
|
|
400
|
+
filteredEntities = filteredEntities.filter(e => e.agentThreadId === threadId);
|
|
401
|
+
}
|
|
402
|
+
// Filter by entity type if specified
|
|
403
|
+
if (entityType) {
|
|
404
|
+
filteredEntities = filteredEntities.filter(e => e.entityType === entityType);
|
|
405
|
+
}
|
|
406
|
+
// Filter by name pattern if specified (case-insensitive)
|
|
407
|
+
if (namePattern) {
|
|
408
|
+
const pattern = namePattern.toLowerCase();
|
|
409
|
+
filteredEntities = filteredEntities.filter(e => e.name.toLowerCase().includes(pattern));
|
|
410
|
+
}
|
|
411
|
+
// Return simplified list with just name and entityType
|
|
412
|
+
return filteredEntities.map(e => ({
|
|
413
|
+
name: e.name,
|
|
414
|
+
entityType: e.entityType
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
206
417
|
// Enhancement 1: Memory Statistics & Insights
|
|
207
418
|
async getMemoryStats() {
|
|
208
419
|
const graph = await this.storage.loadGraph();
|
|
@@ -265,6 +476,19 @@ export class KnowledgeGraphManager {
|
|
|
265
476
|
if (from === to) {
|
|
266
477
|
return { found: true, path: [from], relations: [] };
|
|
267
478
|
}
|
|
479
|
+
// Build indexes for efficient relation lookup
|
|
480
|
+
const relationsFrom = new Map();
|
|
481
|
+
const relationsTo = new Map();
|
|
482
|
+
for (const rel of graph.relations) {
|
|
483
|
+
if (!relationsFrom.has(rel.from)) {
|
|
484
|
+
relationsFrom.set(rel.from, []);
|
|
485
|
+
}
|
|
486
|
+
relationsFrom.get(rel.from).push(rel);
|
|
487
|
+
if (!relationsTo.has(rel.to)) {
|
|
488
|
+
relationsTo.set(rel.to, []);
|
|
489
|
+
}
|
|
490
|
+
relationsTo.get(rel.to).push(rel);
|
|
491
|
+
}
|
|
268
492
|
// BFS to find shortest path
|
|
269
493
|
const queue = [
|
|
270
494
|
{ entity: from, path: [from], relations: [] }
|
|
@@ -276,8 +500,8 @@ export class KnowledgeGraphManager {
|
|
|
276
500
|
continue;
|
|
277
501
|
}
|
|
278
502
|
// Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
|
|
279
|
-
const outgoing =
|
|
280
|
-
const incoming =
|
|
503
|
+
const outgoing = relationsFrom.get(current.entity) || [];
|
|
504
|
+
const incoming = relationsTo.get(current.entity) || [];
|
|
281
505
|
// Check outgoing relations
|
|
282
506
|
for (const rel of outgoing) {
|
|
283
507
|
if (rel.to === to) {
|
|
@@ -335,8 +559,8 @@ export class KnowledgeGraphManager {
|
|
|
335
559
|
continue;
|
|
336
560
|
}
|
|
337
561
|
// Check for negation patterns
|
|
338
|
-
const obs1HasNegation =
|
|
339
|
-
const obs2HasNegation =
|
|
562
|
+
const obs1HasNegation = this.hasNegation(obs1Content);
|
|
563
|
+
const obs2HasNegation = this.hasNegation(obs2Content);
|
|
340
564
|
// If one has negation and they share key words, might be a conflict
|
|
341
565
|
if (obs1HasNegation !== obs2HasNegation) {
|
|
342
566
|
const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
|
|
@@ -9,18 +9,57 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
* Saves entities and their relations to the knowledge graph atomically
|
|
10
10
|
* Either all entities + relations succeed, or none are saved (rollback)
|
|
11
11
|
*/
|
|
12
|
-
export async function handleSaveMemory(input, createEntitiesFn, createRelationsFn) {
|
|
12
|
+
export async function handleSaveMemory(input, createEntitiesFn, createRelationsFn, getExistingEntityNamesFn) {
|
|
13
13
|
const timestamp = new Date().toISOString();
|
|
14
|
-
//
|
|
15
|
-
|
|
14
|
+
// Get existing entity names for cross-thread reference validation
|
|
15
|
+
let existingEntityNames;
|
|
16
|
+
if (getExistingEntityNamesFn) {
|
|
17
|
+
try {
|
|
18
|
+
existingEntityNames = await getExistingEntityNamesFn(input.threadId);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// If we can't get existing entities, proceed without cross-thread validation
|
|
22
|
+
console.warn(`Failed to get existing entities for thread ${input.threadId}:`, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Validate the entire request (with cross-thread entity reference support)
|
|
26
|
+
const validationResult = validateSaveMemoryRequest(input.entities, existingEntityNames);
|
|
16
27
|
if (!validationResult.valid) {
|
|
17
|
-
//
|
|
28
|
+
// Group errors by entity for better structure
|
|
29
|
+
const errorsByEntity = new Map();
|
|
30
|
+
for (const err of validationResult.errors) {
|
|
31
|
+
if (!errorsByEntity.has(err.entityIndex)) {
|
|
32
|
+
errorsByEntity.set(err.entityIndex, {
|
|
33
|
+
entity_name: err.entity,
|
|
34
|
+
entity_type: err.entityType,
|
|
35
|
+
errors: [],
|
|
36
|
+
observations: []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const entityErrors = errorsByEntity.get(err.entityIndex);
|
|
40
|
+
const errorMsg = err.suggestion
|
|
41
|
+
? `${err.error} Suggestion: ${err.suggestion}`
|
|
42
|
+
: err.error;
|
|
43
|
+
entityErrors.errors.push(errorMsg);
|
|
44
|
+
if (err.observationPreview) {
|
|
45
|
+
entityErrors.observations.push(err.observationPreview);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Convert to structured format
|
|
49
|
+
const structuredErrors = Array.from(errorsByEntity.entries()).map(([index, data]) => ({
|
|
50
|
+
entity_index: index,
|
|
51
|
+
entity_name: data.entity_name,
|
|
52
|
+
entity_type: data.entity_type,
|
|
53
|
+
errors: data.errors,
|
|
54
|
+
observations: data.observations.length > 0 ? data.observations : undefined
|
|
55
|
+
}));
|
|
56
|
+
// Return validation errors with detailed structure
|
|
18
57
|
return {
|
|
19
58
|
success: false,
|
|
20
59
|
created: { entities: 0, relations: 0 },
|
|
21
60
|
warnings: [],
|
|
22
61
|
quality_score: 0,
|
|
23
|
-
validation_errors:
|
|
62
|
+
validation_errors: structuredErrors
|
|
24
63
|
};
|
|
25
64
|
}
|
|
26
65
|
try {
|
|
@@ -77,11 +116,14 @@ export async function handleSaveMemory(input, createEntitiesFn, createRelationsF
|
|
|
77
116
|
const createdRelations = await createRelationsFn(relations);
|
|
78
117
|
// Calculate quality score
|
|
79
118
|
const qualityScore = calculateQualityScore(input.entities);
|
|
119
|
+
// Extract entity names for reference in subsequent calls
|
|
120
|
+
const entityNames = createdEntities.map(e => e.name);
|
|
80
121
|
return {
|
|
81
122
|
success: true,
|
|
82
123
|
created: {
|
|
83
124
|
entities: createdEntities.length,
|
|
84
|
-
relations: createdRelations.length
|
|
125
|
+
relations: createdRelations.length,
|
|
126
|
+
entity_names: entityNames
|
|
85
127
|
},
|
|
86
128
|
warnings: validationResult.warnings,
|
|
87
129
|
quality_score: qualityScore
|
package/dist/lib/schemas.js
CHANGED
|
@@ -42,7 +42,7 @@ export const SaveMemoryRelationSchema = z.object({
|
|
|
42
42
|
export const SaveMemoryEntitySchema = z.object({
|
|
43
43
|
name: z.string().min(1).max(100).describe("Unique identifier for the entity"),
|
|
44
44
|
entityType: z.string().min(1).max(50).describe("Type of entity (e.g., Person, Document, File, or custom types like Patient, API). Convention: start with capital letter."),
|
|
45
|
-
observations: z.array(z.string().min(5).max(
|
|
45
|
+
observations: z.array(z.string().min(5).max(300).describe("Atomic fact, max 300 chars (increased to accommodate technical content)")).min(1).describe("Array of atomic facts. Each must be ONE fact, max 300 chars."),
|
|
46
46
|
relations: z.array(SaveMemoryRelationSchema)
|
|
47
47
|
.min(1)
|
|
48
48
|
.describe("REQUIRED: Every entity must have at least 1 relation"),
|
|
@@ -57,7 +57,8 @@ export const SaveMemoryOutputSchema = z.object({
|
|
|
57
57
|
success: z.boolean(),
|
|
58
58
|
created: z.object({
|
|
59
59
|
entities: z.number(),
|
|
60
|
-
relations: z.number()
|
|
60
|
+
relations: z.number(),
|
|
61
|
+
entity_names: z.array(z.string()).optional().describe("Names of created entities (for reference in subsequent calls)")
|
|
61
62
|
}),
|
|
62
63
|
warnings: z.array(z.string()),
|
|
63
64
|
quality_score: z.number().min(0).max(1),
|
|
@@ -100,3 +101,46 @@ export const GetObservationHistoryInputSchema = z.object({
|
|
|
100
101
|
export const GetObservationHistoryOutputSchema = z.object({
|
|
101
102
|
history: z.array(ObservationSchema).describe("Full version chain of the observation, chronologically ordered")
|
|
102
103
|
});
|
|
104
|
+
// Schema for list_entities tool (Simple Entity Lookup)
|
|
105
|
+
export const ListEntitiesInputSchema = z.object({
|
|
106
|
+
threadId: z.string().optional().describe("Filter by thread ID (optional - returns entities from all threads if not specified)"),
|
|
107
|
+
entityType: z.string().optional().describe("Filter by entity type (e.g., 'Person', 'Service', 'Document')"),
|
|
108
|
+
namePattern: z.string().optional().describe("Filter by name pattern (case-insensitive substring match)")
|
|
109
|
+
});
|
|
110
|
+
export const ListEntitiesOutputSchema = z.object({
|
|
111
|
+
entities: z.array(z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
entityType: z.string()
|
|
114
|
+
})).describe("List of entities matching the filters")
|
|
115
|
+
});
|
|
116
|
+
// Schema for validate_memory tool (Pre-Validation)
|
|
117
|
+
export const ValidateMemoryInputSchema = z.object({
|
|
118
|
+
entities: z.array(SaveMemoryEntitySchema).min(1).describe("Array of entities to validate"),
|
|
119
|
+
threadId: z.string().min(1).describe("Thread ID for this conversation/project")
|
|
120
|
+
});
|
|
121
|
+
export const ValidateMemoryOutputSchema = z.object({
|
|
122
|
+
all_valid: z.boolean().describe("True if all entities pass validation"),
|
|
123
|
+
results: z.array(z.object({
|
|
124
|
+
index: z.number().describe("Entity index in the input array"),
|
|
125
|
+
name: z.string().describe("Entity name"),
|
|
126
|
+
type: z.string().describe("Entity type"),
|
|
127
|
+
valid: z.boolean().describe("True if this entity passes validation"),
|
|
128
|
+
errors: z.array(z.string()).describe("List of validation errors"),
|
|
129
|
+
warnings: z.array(z.string()).describe("List of validation warnings")
|
|
130
|
+
})).describe("Validation results for each entity")
|
|
131
|
+
});
|
|
132
|
+
// Schema for update_observation tool
|
|
133
|
+
export const UpdateObservationInputSchema = z.object({
|
|
134
|
+
entityName: z.string().min(1).describe("Name of the entity containing the observation"),
|
|
135
|
+
observationId: z.string().min(1).describe("ID of the observation to update"),
|
|
136
|
+
newContent: z.string().min(1).max(300).describe("New content for the observation (max 300 chars). Minimum 1 character to allow short but valid observations like abbreviations or single words."),
|
|
137
|
+
agentThreadId: z.string().min(1).describe("Agent thread ID making this update"),
|
|
138
|
+
timestamp: z.string().describe("ISO 8601 timestamp of the update"),
|
|
139
|
+
confidence: z.number().min(0).max(1).optional().describe("Optional confidence score (0-1), inherits from old observation if not provided"),
|
|
140
|
+
importance: z.number().min(0).max(1).optional().describe("Optional importance score (0-1), inherits from old observation if not provided")
|
|
141
|
+
});
|
|
142
|
+
export const UpdateObservationOutputSchema = z.object({
|
|
143
|
+
success: z.boolean(),
|
|
144
|
+
updatedObservation: ObservationSchema.describe("The new version of the observation"),
|
|
145
|
+
message: z.string()
|
|
146
|
+
});
|
package/dist/lib/validation.js
CHANGED
|
@@ -3,24 +3,39 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { MAX_OBSERVATION_LENGTH, MIN_OBSERVATION_LENGTH, MAX_SENTENCES, SENTENCE_TERMINATORS, TARGET_AVG_RELATIONS, RELATION_SCORE_WEIGHT, OBSERVATION_SCORE_WEIGHT } from './constants.js';
|
|
5
5
|
/**
|
|
6
|
-
* Counts actual sentences in text, ignoring periods in
|
|
6
|
+
* Counts actual sentences in text, ignoring periods in technical content
|
|
7
7
|
* @param text The text to analyze
|
|
8
8
|
* @returns Number of actual sentences
|
|
9
9
|
*/
|
|
10
10
|
function countSentences(text) {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// Patterns to ignore - technical content that contains periods but aren't sentence boundaries
|
|
12
|
+
// NOTE: Order matters! More specific patterns (multi-letter abbreviations) must come before more general patterns
|
|
13
|
+
const patternsToIgnore = [
|
|
14
|
+
/https?:\/\/[^\s]+/g, // URLs (http:// or https://) - allows periods in paths
|
|
15
|
+
/\b\d+\.\d+\.\d+\.\d+\b/g, // IP addresses (e.g., 192.168.1.1)
|
|
16
|
+
/\b[A-Za-z]:[\\\/](?:[^\s<>:"|?*]+(?:\s+[^\s<>:"|?*]+)*)/g, // Windows/Unix paths (handles spaces, e.g., C:\Program Files\...)
|
|
17
|
+
/\b[vV]?\d+\.\d+(\.\d+)*\b/g, // Version numbers (e.g., v1.2.0, 5.4.3)
|
|
18
|
+
/\b(?:[A-Z]\.){2,}/g, // Multi-letter abbreviations (e.g., U.S., U.K., U.S.A., P.D.F., I.B.M., etc.) - must come before single-letter pattern
|
|
19
|
+
/\b[A-Z][a-z]{0,3}\./g, // Common single-letter abbreviations (e.g., Dr., Mr., Mrs., Ms., Jr., Sr., etc.)
|
|
20
|
+
/\b[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?){2,}\b/g, // Hostnames/domains with at least 2 dots (e.g., sub.domain.com) - must come after all abbreviation patterns
|
|
21
|
+
];
|
|
22
|
+
// Replace technical patterns with placeholders to prevent false sentence detection
|
|
23
|
+
let cleaned = text;
|
|
24
|
+
for (const pattern of patternsToIgnore) {
|
|
25
|
+
cleaned = cleaned.replace(pattern, 'PLACEHOLDER');
|
|
26
|
+
}
|
|
27
|
+
// Split on actual sentence terminators and count non-empty sentences
|
|
17
28
|
const sentences = cleaned.split(SENTENCE_TERMINATORS).filter(s => s.trim().length > 0);
|
|
18
29
|
return sentences.length;
|
|
19
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Maximum length for observation preview in error messages
|
|
33
|
+
*/
|
|
34
|
+
const OBSERVATION_PREVIEW_LENGTH = 50;
|
|
20
35
|
/**
|
|
21
36
|
* Validates a single observation according to spec requirements:
|
|
22
37
|
* - Min 5 characters
|
|
23
|
-
* - Max
|
|
38
|
+
* - Max 300 characters (increased to accommodate technical content)
|
|
24
39
|
* - Max 3 sentences (ignoring periods in version numbers and decimals)
|
|
25
40
|
*/
|
|
26
41
|
export function validateObservation(obs) {
|
|
@@ -35,7 +50,7 @@ export function validateObservation(obs) {
|
|
|
35
50
|
return {
|
|
36
51
|
valid: false,
|
|
37
52
|
error: `Observation too long (${obs.length} chars). Max ${MAX_OBSERVATION_LENGTH}.`,
|
|
38
|
-
suggestion: `Split into
|
|
53
|
+
suggestion: `Split into atomic facts.`
|
|
39
54
|
};
|
|
40
55
|
}
|
|
41
56
|
const sentenceCount = countSentences(obs);
|
|
@@ -62,15 +77,20 @@ export function validateEntityRelations(entity) {
|
|
|
62
77
|
return { valid: true };
|
|
63
78
|
}
|
|
64
79
|
/**
|
|
65
|
-
* Validates that relation targets exist in the same request
|
|
80
|
+
* Validates that relation targets exist in the same request or in existing entities
|
|
81
|
+
* @param entity The entity whose relations to validate
|
|
82
|
+
* @param allEntityNames Set of entity names in the current request
|
|
83
|
+
* @param existingEntityNames Optional set of entity names that already exist in storage (for cross-thread references)
|
|
66
84
|
*/
|
|
67
|
-
export function validateRelationTargets(entity, allEntityNames) {
|
|
85
|
+
export function validateRelationTargets(entity, allEntityNames, existingEntityNames) {
|
|
68
86
|
for (const relation of entity.relations) {
|
|
69
|
-
|
|
87
|
+
const targetInCurrentBatch = allEntityNames.has(relation.targetEntity);
|
|
88
|
+
const targetInExisting = existingEntityNames?.has(relation.targetEntity) ?? false;
|
|
89
|
+
if (!targetInCurrentBatch && !targetInExisting) {
|
|
70
90
|
return {
|
|
71
91
|
valid: false,
|
|
72
|
-
error: `Target entity '${relation.targetEntity}' not found in request`,
|
|
73
|
-
suggestion: `targetEntity must reference another entity in the same save_memory call`
|
|
92
|
+
error: `Target entity '${relation.targetEntity}' not found in request or existing entities`,
|
|
93
|
+
suggestion: `targetEntity must reference another entity in the same save_memory call or an existing entity`
|
|
74
94
|
};
|
|
75
95
|
}
|
|
76
96
|
}
|
|
@@ -99,15 +119,23 @@ export function normalizeEntityType(entityType) {
|
|
|
99
119
|
}
|
|
100
120
|
return { normalized, warnings };
|
|
101
121
|
}
|
|
102
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Validates all aspects of a save_memory request
|
|
124
|
+
* @param entities The entities to validate
|
|
125
|
+
* @param existingEntityNames Optional set of entity names that already exist in storage (for cross-thread references)
|
|
126
|
+
*/
|
|
127
|
+
export function validateSaveMemoryRequest(entities, existingEntityNames) {
|
|
103
128
|
const errors = [];
|
|
104
129
|
const warnings = [];
|
|
105
130
|
// Collect all entity names for relation validation
|
|
106
131
|
const entityNames = new Set(entities.map(e => e.name));
|
|
107
|
-
for (
|
|
132
|
+
for (let entityIndex = 0; entityIndex < entities.length; entityIndex++) {
|
|
133
|
+
const entity = entities[entityIndex];
|
|
108
134
|
// Validate entity type and collect warnings
|
|
135
|
+
// Note: entityType is normalized in-place for consistency with existing behavior
|
|
136
|
+
// The normalized value is used throughout the rest of validation and saving
|
|
109
137
|
const { normalized, warnings: typeWarnings } = normalizeEntityType(entity.entityType);
|
|
110
|
-
entity.entityType = normalized; // Apply normalization
|
|
138
|
+
entity.entityType = normalized; // Apply normalization (intentional mutation for consistency)
|
|
111
139
|
warnings.push(...typeWarnings);
|
|
112
140
|
// Validate observations (note: observations are still strings in SaveMemoryEntity input)
|
|
113
141
|
for (let i = 0; i < entity.observations.length; i++) {
|
|
@@ -115,8 +143,12 @@ export function validateSaveMemoryRequest(entities) {
|
|
|
115
143
|
if (!obsResult.valid) {
|
|
116
144
|
errors.push({
|
|
117
145
|
entity: entity.name,
|
|
146
|
+
entityIndex: entityIndex,
|
|
147
|
+
entityType: entity.entityType,
|
|
118
148
|
error: `Observation ${i + 1}: ${obsResult.error}`,
|
|
119
|
-
suggestion: obsResult.suggestion
|
|
149
|
+
suggestion: obsResult.suggestion,
|
|
150
|
+
observationPreview: entity.observations[i].substring(0, OBSERVATION_PREVIEW_LENGTH) +
|
|
151
|
+
(entity.observations[i].length > OBSERVATION_PREVIEW_LENGTH ? '...' : '')
|
|
120
152
|
});
|
|
121
153
|
}
|
|
122
154
|
}
|
|
@@ -125,15 +157,19 @@ export function validateSaveMemoryRequest(entities) {
|
|
|
125
157
|
if (!relResult.valid) {
|
|
126
158
|
errors.push({
|
|
127
159
|
entity: entity.name,
|
|
160
|
+
entityIndex: entityIndex,
|
|
161
|
+
entityType: entity.entityType,
|
|
128
162
|
error: relResult.error || 'Invalid relations',
|
|
129
163
|
suggestion: relResult.suggestion
|
|
130
164
|
});
|
|
131
165
|
}
|
|
132
|
-
// Validate relation targets
|
|
133
|
-
const targetResult = validateRelationTargets(entity, entityNames);
|
|
166
|
+
// Validate relation targets (now supports cross-thread references)
|
|
167
|
+
const targetResult = validateRelationTargets(entity, entityNames, existingEntityNames);
|
|
134
168
|
if (!targetResult.valid) {
|
|
135
169
|
errors.push({
|
|
136
170
|
entity: entity.name,
|
|
171
|
+
entityIndex: entityIndex,
|
|
172
|
+
entityType: entity.entityType,
|
|
137
173
|
error: targetResult.error || 'Invalid relation target',
|
|
138
174
|
suggestion: targetResult.suggestion
|
|
139
175
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-memory-enhanced",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Enhanced MCP server for memory with agent threading, timestamps, and confidence scoring",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",
|