persyst-mcp 2.2.3 → 2.2.5
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 +39 -0
- package/bin/export.js +116 -0
- package/bin/import.js +160 -0
- package/hooks/persyst-hook.js +9 -10
- package/index.js +11 -1
- package/package.json +13 -3
- package/src/database.js +37 -19
- package/src/events.js +19 -0
- package/src/extractor-heuristic.js +502 -324
- package/src/sdk.d.ts +175 -0
- package/src/sdk.js +217 -0
- package/src/search.js +103 -7
- package/src/server.js +723 -183
- package/src/tools.js +14 -6
- package/src/watcher.js +27 -12
package/src/sdk.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript type declarations for the Persyst Developer SDK.
|
|
3
|
+
* Import via: import { Persyst } from 'persyst-mcp/sdk'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PersystConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Force a connection mode. If omitted, Persyst auto-detects.
|
|
9
|
+
* - `'gateway'` — connect to the local HTTP gateway on port 4321
|
|
10
|
+
* - `'library'` — use in-process SQLite directly (no server needed)
|
|
11
|
+
* - `null` — auto-detect: probe gateway first, fall back to library
|
|
12
|
+
*/
|
|
13
|
+
mode?: 'gateway' | 'library' | null;
|
|
14
|
+
/** Gateway host (default: '127.0.0.1') */
|
|
15
|
+
host?: string;
|
|
16
|
+
/** Gateway port (default: 4321) */
|
|
17
|
+
port?: number;
|
|
18
|
+
/** Optional API key for gateway authorization */
|
|
19
|
+
apiKey?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TrackOptions {
|
|
23
|
+
/** Active session or thread identifier */
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
/** @alias sessionId */
|
|
26
|
+
session_id?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Active workflow name (used as agent_id for namespace isolation).
|
|
29
|
+
* Example: 'customer_support', 'code_review'
|
|
30
|
+
*/
|
|
31
|
+
workflow?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Specific event name. Used to build `content` if `content` is not provided.
|
|
34
|
+
* Example: 'payment_failed', 'build_passed'
|
|
35
|
+
*/
|
|
36
|
+
event?: string;
|
|
37
|
+
/** Full text content to store as the memory. Required if `event` is not provided. */
|
|
38
|
+
content?: string;
|
|
39
|
+
/** Structured metadata to append to the generated content string. */
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
/** Importance score from 0.0 (low) to 1.0 (high). Default: 1.0 */
|
|
42
|
+
importance?: number;
|
|
43
|
+
/**
|
|
44
|
+
* If true (default), the memory is visible to all agents.
|
|
45
|
+
* If false, it is isolated to the `workflow` agent's namespace.
|
|
46
|
+
*/
|
|
47
|
+
shared?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TrackResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
/** The ID of the stored (or existing) memory */
|
|
53
|
+
id: number;
|
|
54
|
+
/** The namespace the memory was written to */
|
|
55
|
+
namespace: string;
|
|
56
|
+
/** Human-readable result message */
|
|
57
|
+
message?: string;
|
|
58
|
+
/** Error message, present only on failure */
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProvenanceRecord {
|
|
63
|
+
source_type: 'agent' | 'git' | 'api' | 'import' | 'manual';
|
|
64
|
+
source_id: string | null;
|
|
65
|
+
confidence: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface MemoryRecord {
|
|
69
|
+
id: number;
|
|
70
|
+
content: string;
|
|
71
|
+
importance_score: number;
|
|
72
|
+
created_at: number;
|
|
73
|
+
last_accessed: number;
|
|
74
|
+
/** Relevance score (hybrid search + reputation weight) */
|
|
75
|
+
score: number;
|
|
76
|
+
provenance?: ProvenanceRecord | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ContextOptions {
|
|
80
|
+
/** Active session or thread identifier */
|
|
81
|
+
sessionId?: string;
|
|
82
|
+
/** @alias sessionId */
|
|
83
|
+
session_id?: string;
|
|
84
|
+
/** Active workflow / agent name */
|
|
85
|
+
workflow?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Hint for the active task intent. Used to refine context selection.
|
|
88
|
+
* Example: 'debugging', 'ui_styling', 'database_management', 'deployment'
|
|
89
|
+
*/
|
|
90
|
+
intent?: string;
|
|
91
|
+
/** Search query string — required */
|
|
92
|
+
query: string;
|
|
93
|
+
/** Hard token budget for the returned context block. Default: 2000 */
|
|
94
|
+
maxTokens?: number;
|
|
95
|
+
/** @alias maxTokens */
|
|
96
|
+
max_tokens?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ContextResult {
|
|
100
|
+
/**
|
|
101
|
+
* A formatted, ready-to-inject context string for LLM system prompts.
|
|
102
|
+
* Contains memory entries ranked by relevance within the token budget.
|
|
103
|
+
*/
|
|
104
|
+
context: string;
|
|
105
|
+
/** The ranked memory records included in the context */
|
|
106
|
+
memories: MemoryRecord[];
|
|
107
|
+
/** Cryptographic Ed25519 attestation record for audit trails */
|
|
108
|
+
attestation: object | null;
|
|
109
|
+
/** Detected query intent classification */
|
|
110
|
+
intent: string;
|
|
111
|
+
/** Detected urgency level based on query language */
|
|
112
|
+
urgency: 'low' | 'medium' | 'high' | 'critical';
|
|
113
|
+
/** Generated actionable suggestions derived from retrieved memories */
|
|
114
|
+
suggested_actions: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Persyst Developer SDK Client.
|
|
119
|
+
*
|
|
120
|
+
* Supports two transport modes:
|
|
121
|
+
* - **Gateway Mode**: communicates with the local HTTP gateway on port 4321.
|
|
122
|
+
* Best for Python/other-language agents, or when the server is already running.
|
|
123
|
+
* - **Library Mode**: directly accesses the local SQLite database in-process.
|
|
124
|
+
* Best for Node.js scripts and when no server is needed.
|
|
125
|
+
*
|
|
126
|
+
* Auto-detects the available mode on first call (150ms probe timeout).
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import { Persyst } from 'persyst-mcp/sdk';
|
|
131
|
+
*
|
|
132
|
+
* const persyst = new Persyst();
|
|
133
|
+
*
|
|
134
|
+
* // Track an architectural decision
|
|
135
|
+
* await persyst.track({
|
|
136
|
+
* content: 'We use TypeScript for all new source files',
|
|
137
|
+
* importance: 0.9,
|
|
138
|
+
* workflow: 'my-agent'
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Retrieve compressed context for an LLM
|
|
142
|
+
* const { context } = await persyst.context({
|
|
143
|
+
* query: 'coding conventions and stack choices',
|
|
144
|
+
* intent: 'general'
|
|
145
|
+
* });
|
|
146
|
+
* console.log(context);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare class Persyst {
|
|
150
|
+
constructor(config?: PersystConfig);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Track a developer event or milestone.
|
|
154
|
+
*
|
|
155
|
+
* Stores it as a persistent memory in the local SQLite database via
|
|
156
|
+
* the gateway (HTTP) or directly (library mode).
|
|
157
|
+
*
|
|
158
|
+
* If an identical memory already exists, its importance is boosted instead
|
|
159
|
+
* of creating a duplicate.
|
|
160
|
+
*
|
|
161
|
+
* @throws {Error} If neither `content` nor `event` is provided.
|
|
162
|
+
*/
|
|
163
|
+
track(eventData: TrackOptions): Promise<TrackResult>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Retrieve compiled, optimized context tailored by query and intent.
|
|
167
|
+
*
|
|
168
|
+
* Runs hybrid search (keyword + semantic) + knowledge graph traversal,
|
|
169
|
+
* applies temporal decay + agent reputation weighting, then compresses
|
|
170
|
+
* the result to fit within `maxTokens`.
|
|
171
|
+
*
|
|
172
|
+
* Returns a formatted context block ready to inject into an LLM system prompt.
|
|
173
|
+
*/
|
|
174
|
+
context(contextQuery: ContextOptions): Promise<ContextResult>;
|
|
175
|
+
}
|
package/src/sdk.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import net from 'net';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Persyst Developer SDK Client
|
|
6
|
+
* Supports both Gateway Mode (local HTTP server) and Library Mode (direct in-process SQLite).
|
|
7
|
+
*/
|
|
8
|
+
export class Persyst {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} [config={}]
|
|
11
|
+
* @param {string} [config.mode=null] - 'gateway' | 'library' | null (auto-detect)
|
|
12
|
+
* @param {string} [config.host='127.0.0.1'] - Gateway host
|
|
13
|
+
* @param {number} [config.port=4321] - Gateway port
|
|
14
|
+
* @param {string} [config.apiKey=null] - Gateway authorization key
|
|
15
|
+
*/
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.mode = config.mode || null;
|
|
18
|
+
this.host = config.host || '127.0.0.1';
|
|
19
|
+
this.port = config.port || 4321;
|
|
20
|
+
this.apiKey = config.apiKey || null;
|
|
21
|
+
this._detectedMode = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Auto-detect reachable Gateway on port 4321, falling back to direct library mode.
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
async _resolveMode() {
|
|
29
|
+
if (this.mode) {
|
|
30
|
+
return this.mode;
|
|
31
|
+
}
|
|
32
|
+
if (this._detectedMode) {
|
|
33
|
+
return this._detectedMode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const isReachable = await new Promise((resolve) => {
|
|
38
|
+
const socket = new net.Socket();
|
|
39
|
+
socket.setTimeout(150); // 150ms probe timeout
|
|
40
|
+
socket.on('connect', () => {
|
|
41
|
+
socket.destroy();
|
|
42
|
+
resolve(true);
|
|
43
|
+
});
|
|
44
|
+
socket.on('error', () => {
|
|
45
|
+
socket.destroy();
|
|
46
|
+
resolve(false);
|
|
47
|
+
});
|
|
48
|
+
socket.on('timeout', () => {
|
|
49
|
+
socket.destroy();
|
|
50
|
+
resolve(false);
|
|
51
|
+
});
|
|
52
|
+
socket.connect(this.port, this.host);
|
|
53
|
+
});
|
|
54
|
+
this._detectedMode = isReachable ? 'gateway' : 'library';
|
|
55
|
+
} catch (_) {
|
|
56
|
+
this._detectedMode = 'library';
|
|
57
|
+
}
|
|
58
|
+
return this._detectedMode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Track a developer event or milestone.
|
|
63
|
+
* @param {Object} eventData
|
|
64
|
+
* @param {string} [eventData.sessionId] - Active session / thread identifier
|
|
65
|
+
* @param {string} [eventData.workflow] - Active workflow name (e.g. 'customer_support')
|
|
66
|
+
* @param {string} [eventData.event] - Specific event name (e.g. 'payment_failed')
|
|
67
|
+
* @param {string} [eventData.content] - Full text detail of the event
|
|
68
|
+
* @param {Object} [eventData.metadata] - Structured metadata facts
|
|
69
|
+
* @param {number} [eventData.importance=1.0] - Importance score (0.0 - 1.0)
|
|
70
|
+
* @param {boolean} [eventData.shared=true] - Whether the memory is shared across namespaces
|
|
71
|
+
*/
|
|
72
|
+
async track(eventData = {}) {
|
|
73
|
+
const mode = await this._resolveMode();
|
|
74
|
+
const sessionId = eventData.sessionId || eventData.session_id || null;
|
|
75
|
+
const workflow = eventData.workflow || null;
|
|
76
|
+
const event = eventData.event || null;
|
|
77
|
+
const metadata = eventData.metadata || null;
|
|
78
|
+
const importance = eventData.importance !== undefined ? eventData.importance : 1.0;
|
|
79
|
+
const shared = eventData.shared !== undefined ? eventData.shared : true;
|
|
80
|
+
|
|
81
|
+
let content = eventData.content || '';
|
|
82
|
+
if (!content) {
|
|
83
|
+
if (event) {
|
|
84
|
+
content = `Event: ${event}`;
|
|
85
|
+
if (workflow) content += ` in workflow: ${workflow}`;
|
|
86
|
+
if (metadata) content += `. Metadata: ${JSON.stringify(metadata)}`;
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error('Either content or event must be provided to track()');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (mode === 'gateway') {
|
|
93
|
+
return this._trackGateway({ content, importance, agent_id: workflow || 'sdk', session_id: sessionId, shared });
|
|
94
|
+
} else {
|
|
95
|
+
return this._trackLibrary({ content, importance, agent_id: workflow || 'sdk', session_id: sessionId, shared });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Internal direct SQLite write.
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
async _trackLibrary({ content, importance, agent_id, session_id, shared }) {
|
|
104
|
+
const { insertMemory, insertVector } = await import('./database.js');
|
|
105
|
+
const { generateEmbedding } = await import('./embeddings.js');
|
|
106
|
+
|
|
107
|
+
const namespace = shared ? 'shared' : agent_id;
|
|
108
|
+
const id = insertMemory(content, importance, {
|
|
109
|
+
source_type: 'api',
|
|
110
|
+
source_id: agent_id,
|
|
111
|
+
confidence: 1.0
|
|
112
|
+
}, namespace);
|
|
113
|
+
|
|
114
|
+
const embedding = await generateEmbedding(content);
|
|
115
|
+
insertVector(id, embedding);
|
|
116
|
+
return { success: true, id };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Internal HTTP POST write to Gateway.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
_trackGateway(payload) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const body = JSON.stringify(payload);
|
|
126
|
+
const req = http.request({
|
|
127
|
+
hostname: this.host,
|
|
128
|
+
port: this.port,
|
|
129
|
+
path: '/add',
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Content-Length': Buffer.byteLength(body)
|
|
134
|
+
}
|
|
135
|
+
}, (res) => {
|
|
136
|
+
let responseBody = '';
|
|
137
|
+
res.on('data', chunk => { responseBody += chunk; });
|
|
138
|
+
res.on('end', () => {
|
|
139
|
+
try {
|
|
140
|
+
resolve(JSON.parse(responseBody));
|
|
141
|
+
} catch (e) {
|
|
142
|
+
reject(new Error(`Failed to parse gateway response: ${e.message}`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
req.on('error', reject);
|
|
147
|
+
req.write(body);
|
|
148
|
+
req.end();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Retrieve compiled, optimized context tailored by intent and workflow.
|
|
154
|
+
* @param {Object} contextQuery
|
|
155
|
+
* @param {string} [contextQuery.sessionId] - Active session / thread identifier
|
|
156
|
+
* @param {string} [contextQuery.workflow] - Active workflow name (e.g. 'customer_support')
|
|
157
|
+
* @param {string} [contextQuery.intent] - Active reasoning intent (e.g. 'debugging')
|
|
158
|
+
* @param {string} contextQuery.query - Prompt query string to find similar context for
|
|
159
|
+
* @param {number} [contextQuery.maxTokens=2000] - Hard budget limit of tokens
|
|
160
|
+
*/
|
|
161
|
+
async context(contextQuery = {}) {
|
|
162
|
+
const mode = await this._resolveMode();
|
|
163
|
+
const sessionId = contextQuery.sessionId || contextQuery.session_id || null;
|
|
164
|
+
const workflow = contextQuery.workflow || null;
|
|
165
|
+
const intent = contextQuery.intent || null;
|
|
166
|
+
const query = contextQuery.query || '';
|
|
167
|
+
const maxTokens = contextQuery.maxTokens || contextQuery.max_tokens || 2000;
|
|
168
|
+
|
|
169
|
+
if (mode === 'gateway') {
|
|
170
|
+
return this._contextGateway({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
|
|
171
|
+
} else {
|
|
172
|
+
return this._contextLibrary({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Internal direct SQLite read.
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
async _contextLibrary({ query, max_tokens, agent_id, session_id, intent }) {
|
|
181
|
+
const { getOptimizedContext } = await import('./search.js');
|
|
182
|
+
return getOptimizedContext(query, max_tokens, agent_id, session_id, agent_id || null, intent);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Internal HTTP POST read from Gateway.
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_contextGateway(payload) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const body = JSON.stringify(payload);
|
|
192
|
+
const req = http.request({
|
|
193
|
+
hostname: this.host,
|
|
194
|
+
port: this.port,
|
|
195
|
+
path: '/context',
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: {
|
|
198
|
+
'Content-Type': 'application/json',
|
|
199
|
+
'Content-Length': Buffer.byteLength(body)
|
|
200
|
+
}
|
|
201
|
+
}, (res) => {
|
|
202
|
+
let responseBody = '';
|
|
203
|
+
res.on('data', chunk => { responseBody += chunk; });
|
|
204
|
+
res.on('end', () => {
|
|
205
|
+
try {
|
|
206
|
+
resolve(JSON.parse(responseBody));
|
|
207
|
+
} catch (e) {
|
|
208
|
+
reject(new Error(`Failed to parse gateway response: ${e.message}`));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
req.on('error', reject);
|
|
213
|
+
req.write(body);
|
|
214
|
+
req.end();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/search.js
CHANGED
|
@@ -33,6 +33,12 @@ let lastDataVersion = 0;
|
|
|
33
33
|
* @returns {Promise<Array>} Ranked search results (with .attestation property attached)
|
|
34
34
|
*/
|
|
35
35
|
export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null, skipAttestation = false) {
|
|
36
|
+
if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
|
|
37
|
+
throw new Error('Limit must be a positive integer.');
|
|
38
|
+
}
|
|
39
|
+
const parsedLimit = Math.floor(limit);
|
|
40
|
+
const ns = namespace || 'shared';
|
|
41
|
+
|
|
36
42
|
// Sync in-memory cache with external DB changes using sqlite data_version
|
|
37
43
|
try {
|
|
38
44
|
const currentDataVersion = db.pragma('data_version', { simple: true });
|
|
@@ -46,7 +52,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
46
52
|
|
|
47
53
|
// --- Check LRU cache first (Feature 1) ---
|
|
48
54
|
// Include namespace in cache key to prevent cross-namespace cache hits
|
|
49
|
-
const cacheKey = LRUCache.key(`${
|
|
55
|
+
const cacheKey = LRUCache.key(`${ns}:${queryText}`, parsedLimit);
|
|
50
56
|
const cached = searchCache.get(cacheKey);
|
|
51
57
|
if (cached) {
|
|
52
58
|
console.error(`[persyst-cache] Cache HIT for query: "${queryText.slice(0, 50)}..."`);
|
|
@@ -54,12 +60,12 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
// --- Step 1: Keyword search (fast, exact matches) ---
|
|
57
|
-
const keywordHits = searchKeyword(queryText,
|
|
63
|
+
const keywordHits = searchKeyword(queryText, parsedLimit * 2);
|
|
58
64
|
const keywordIds = new Set(keywordHits.map(r => r.id));
|
|
59
65
|
|
|
60
66
|
// --- Step 2: Semantic search (meaning-based) ---
|
|
61
67
|
const queryEmbedding = await generateEmbedding(queryText);
|
|
62
|
-
const vecHits = searchVector(queryEmbedding,
|
|
68
|
+
const vecHits = searchVector(queryEmbedding, parsedLimit * 2);
|
|
63
69
|
|
|
64
70
|
const semanticResults = vecHits.map(r => ({
|
|
65
71
|
id: r.rowid,
|
|
@@ -99,7 +105,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
99
105
|
const finalResults = combined
|
|
100
106
|
.map(r => {
|
|
101
107
|
// Use namespace-aware getMemoryById to filter by agent namespace
|
|
102
|
-
const memory = getMemoryById(r.id,
|
|
108
|
+
const memory = getMemoryById(r.id, ns);
|
|
103
109
|
if (!memory) return null; // Memory was archived, deleted, or not in namespace
|
|
104
110
|
|
|
105
111
|
// Boost memory access metrics
|
|
@@ -141,7 +147,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
141
147
|
finalResults.sort((a, b) => parseFloat(b.hybrid_score) - parseFloat(a.hybrid_score));
|
|
142
148
|
|
|
143
149
|
// --- Step 5: Apply MMR for diverse retrieval (Feature 3) ---
|
|
144
|
-
const mmrResults = applyMMR(finalResults,
|
|
150
|
+
const mmrResults = applyMMR(finalResults, parsedLimit);
|
|
145
151
|
|
|
146
152
|
// Generate cryptographic attestation for audit trails (skip if called internally)
|
|
147
153
|
let attestation = null;
|
|
@@ -240,7 +246,7 @@ function jaccardSimilarity(a, b) {
|
|
|
240
246
|
* @param {string|null} agentId - Querying agent identifier
|
|
241
247
|
* @param {string|null} sessionId - Current session ID
|
|
242
248
|
*/
|
|
243
|
-
export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null) {
|
|
249
|
+
export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null, intentParam = null) {
|
|
244
250
|
// Extract entities mentioned in the query text to seed the graph search directly
|
|
245
251
|
const entities = getAllEntities(100);
|
|
246
252
|
const matchedEntityIds = new Set();
|
|
@@ -417,8 +423,23 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
417
423
|
accepted.push(c);
|
|
418
424
|
}
|
|
419
425
|
|
|
426
|
+
// Classify intent and urgency based on query text and parameters
|
|
427
|
+
const { intent, urgency } = classifyIntentAndUrgency(queryText, intentParam);
|
|
428
|
+
const suggested_actions = generateSuggestedActions(accepted, intent, urgency);
|
|
429
|
+
|
|
420
430
|
// 6. Format LLM injection context string
|
|
421
431
|
let context = '=== RETRIEVED AGENT MEMORY CONTEXT ===\n';
|
|
432
|
+
context += `[Intent: ${intent} | Urgency: ${urgency}]\n\n`;
|
|
433
|
+
|
|
434
|
+
if (suggested_actions.length > 0) {
|
|
435
|
+
context += '[Suggested Actions]\n';
|
|
436
|
+
for (const action of suggested_actions) {
|
|
437
|
+
context += `• ${action}\n`;
|
|
438
|
+
}
|
|
439
|
+
context += '\n';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
context += '[Memories]\n';
|
|
422
443
|
if (accepted.length === 0) {
|
|
423
444
|
context += 'No relevant memories retrieved.\n';
|
|
424
445
|
} else {
|
|
@@ -441,7 +462,10 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
441
462
|
return {
|
|
442
463
|
context,
|
|
443
464
|
memories: accepted,
|
|
444
|
-
attestation
|
|
465
|
+
attestation,
|
|
466
|
+
intent,
|
|
467
|
+
urgency,
|
|
468
|
+
suggested_actions
|
|
445
469
|
};
|
|
446
470
|
}
|
|
447
471
|
|
|
@@ -625,3 +649,75 @@ export async function consolidateMemories(namespace = null) {
|
|
|
625
649
|
details: consolidated
|
|
626
650
|
};
|
|
627
651
|
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Classify context retrieval intent and urgency level using heuristic analysis.
|
|
655
|
+
*/
|
|
656
|
+
function classifyIntentAndUrgency(queryText, intentParam = null) {
|
|
657
|
+
const queryLower = (queryText || '').toLowerCase();
|
|
658
|
+
|
|
659
|
+
// 1. Determine Intent
|
|
660
|
+
let intent = intentParam || 'general';
|
|
661
|
+
if (intent === 'general' || !intent) {
|
|
662
|
+
if (/(?:db|database|sqlite|sql|table|migration|schema)/i.test(queryLower)) {
|
|
663
|
+
intent = 'database_management';
|
|
664
|
+
} else if (/(?:deploy|ci|cd|vercel|publish|release|prod|staging)/i.test(queryLower)) {
|
|
665
|
+
intent = 'deployment';
|
|
666
|
+
} else if (/(?:style|css|html|theme|design|layout|align|color|font)/i.test(queryLower)) {
|
|
667
|
+
intent = 'ui_styling';
|
|
668
|
+
} else if (/(?:test|spec|unit|mock|heavy|smoke)/i.test(queryLower)) {
|
|
669
|
+
intent = 'testing';
|
|
670
|
+
} else if (/(?:error|bug|fail|crash|break|exception|stack|trace|refused|debug)/i.test(queryLower)) {
|
|
671
|
+
intent = 'debugging';
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 2. Determine Urgency
|
|
676
|
+
let urgency = 'low';
|
|
677
|
+
if (/(?:panic|emergency|broken|critical|urgent|fatal|security|leak|bypass|vulnerability)/i.test(queryLower)) {
|
|
678
|
+
urgency = 'critical';
|
|
679
|
+
} else if (/(?:fail|error|crash|prevent|stop|warn|warning|issue|broken)/i.test(queryLower)) {
|
|
680
|
+
urgency = 'high';
|
|
681
|
+
} else if (/(?:update|change|add|tweak|check|verify)/i.test(queryLower)) {
|
|
682
|
+
urgency = 'medium';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return { intent, urgency };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Generate actionable suggested actions based on active memories and query classification.
|
|
690
|
+
*/
|
|
691
|
+
function generateSuggestedActions(memories, intent, urgency) {
|
|
692
|
+
const actions = [];
|
|
693
|
+
|
|
694
|
+
// General recommendation based on intent
|
|
695
|
+
if (intent === 'debugging') {
|
|
696
|
+
actions.push('Inspect the recent error logs and verify SQLite/system constraints.');
|
|
697
|
+
} else if (intent === 'ui_styling') {
|
|
698
|
+
actions.push('Verify UI layouts conform to user design preferences.');
|
|
699
|
+
} else if (intent === 'database_management') {
|
|
700
|
+
actions.push('Ensure database migrations are applied and referential integrity is checked.');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const m of memories) {
|
|
704
|
+
const content = m.content.toLowerCase();
|
|
705
|
+
|
|
706
|
+
// Check for rules/decisions in memory content
|
|
707
|
+
if (content.includes('decision:') || content.includes('rule:')) {
|
|
708
|
+
actions.push(`Adhere to guideline: ${m.content.slice(0, 100)}...`);
|
|
709
|
+
} else if (content.includes('prefer')) {
|
|
710
|
+
actions.push(`Apply user preference: ${m.content.slice(0, 100)}...`);
|
|
711
|
+
} else if (content.includes('error') || content.includes('bug') || content.includes('fix')) {
|
|
712
|
+
actions.push(`Reference past fix: ${m.content.slice(0, 100)}...`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Safety guideline if critical
|
|
717
|
+
if (urgency === 'critical') {
|
|
718
|
+
actions.unshift('CAUTION: Address security, vulnerability, or critical stability factors immediately.');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Deduplicate
|
|
722
|
+
return Array.from(new Set(actions));
|
|
723
|
+
}
|