code-graph-context 2.6.1 → 2.7.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/dist/core/utils/graph-factory.js +1 -1
- package/dist/mcp/constants.js +75 -2
- package/dist/mcp/handlers/graph-generator.handler.js +1 -0
- package/dist/mcp/mcp.server.js +2 -2
- package/dist/mcp/service-init.js +1 -1
- package/dist/mcp/services/watch-manager.js +24 -10
- package/dist/mcp/tools/index.js +8 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +4 -3
- package/dist/mcp/tools/session-bookmark.tool.js +314 -0
- package/dist/mcp/tools/session-note.tool.js +343 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +4 -11
- package/dist/mcp/tools/swarm-cleanup.tool.js +25 -13
- package/dist/mcp/tools/swarm-complete-task.tool.js +8 -33
- package/dist/mcp/tools/swarm-constants.js +2 -1
- package/dist/mcp/tools/swarm-get-tasks.tool.js +5 -12
- package/dist/mcp/tools/swarm-orchestrate.tool.js +5 -3
- package/dist/mcp/tools/swarm-pheromone.tool.js +11 -4
- package/dist/mcp/tools/swarm-post-task.tool.js +5 -15
- package/dist/mcp/tools/swarm-sense.tool.js +9 -1
- package/dist/storage/neo4j/neo4j.service.js +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Note Tools
|
|
3
|
+
* Save and recall cross-session observations, decisions, and insights
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
|
|
7
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
9
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
10
|
+
const NOTE_CATEGORIES = ['architectural', 'bug', 'insight', 'decision', 'risk', 'todo'];
|
|
11
|
+
const NOTE_SEVERITIES = ['info', 'warning', 'critical'];
|
|
12
|
+
/**
|
|
13
|
+
* Cypher to create a SessionNote node, link it to code nodes via [:ABOUT],
|
|
14
|
+
* and link it to the latest SessionBookmark for the session via [:HAS_NOTE]
|
|
15
|
+
*/
|
|
16
|
+
const CREATE_SESSION_NOTE_QUERY = `
|
|
17
|
+
// Create the SessionNote node
|
|
18
|
+
CREATE (n:SessionNote {
|
|
19
|
+
id: $noteId,
|
|
20
|
+
projectId: $projectId,
|
|
21
|
+
sessionId: $sessionId,
|
|
22
|
+
agentId: $agentId,
|
|
23
|
+
topic: $topic,
|
|
24
|
+
content: $content,
|
|
25
|
+
category: $category,
|
|
26
|
+
severity: $severity,
|
|
27
|
+
createdAt: timestamp(),
|
|
28
|
+
expiresAt: $expiresAt
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Link to referenced code nodes (filter out internal coordination nodes)
|
|
32
|
+
WITH n
|
|
33
|
+
UNWIND CASE WHEN size($aboutNodeIds) = 0 THEN [null] ELSE $aboutNodeIds END AS aboutNodeId
|
|
34
|
+
WITH n, aboutNodeId
|
|
35
|
+
WHERE aboutNodeId IS NOT NULL
|
|
36
|
+
OPTIONAL MATCH (target)
|
|
37
|
+
WHERE target.id = aboutNodeId
|
|
38
|
+
AND target.projectId = $projectId
|
|
39
|
+
AND NOT target:SessionNote
|
|
40
|
+
AND NOT target:SessionBookmark
|
|
41
|
+
AND NOT target:Pheromone
|
|
42
|
+
AND NOT target:SwarmTask
|
|
43
|
+
WITH n, collect(target) AS targets
|
|
44
|
+
FOREACH (t IN targets | MERGE (n)-[:ABOUT]->(t))
|
|
45
|
+
|
|
46
|
+
// Link to the latest SessionBookmark for this session (if one exists)
|
|
47
|
+
WITH n
|
|
48
|
+
OPTIONAL MATCH (bm:SessionBookmark {projectId: $projectId, sessionId: $sessionId})
|
|
49
|
+
WITH n, bm ORDER BY bm.createdAt DESC
|
|
50
|
+
LIMIT 1
|
|
51
|
+
FOREACH (_ IN CASE WHEN bm IS NOT NULL THEN [1] ELSE [] END |
|
|
52
|
+
MERGE (bm)-[:HAS_NOTE]->(n)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
RETURN n.id AS noteId
|
|
56
|
+
`;
|
|
57
|
+
/**
|
|
58
|
+
* Cypher to store the embedding vector on an existing SessionNote node
|
|
59
|
+
*/
|
|
60
|
+
const SET_NOTE_EMBEDDING_QUERY = `
|
|
61
|
+
MATCH (n:SessionNote {id: $noteId, projectId: $projectId})
|
|
62
|
+
SET n.embedding = $embedding
|
|
63
|
+
RETURN n.id AS noteId
|
|
64
|
+
`;
|
|
65
|
+
/**
|
|
66
|
+
* Semantic (vector) search for session notes
|
|
67
|
+
*/
|
|
68
|
+
const VECTOR_SEARCH_NOTES_QUERY = `
|
|
69
|
+
CALL db.index.vector.queryNodes('session_notes_idx', toInteger($limit * 10), $queryEmbedding)
|
|
70
|
+
YIELD node AS n, score
|
|
71
|
+
WHERE n.projectId = $projectId
|
|
72
|
+
AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
|
|
73
|
+
AND ($category IS NULL OR n.category = $category)
|
|
74
|
+
AND ($severity IS NULL OR n.severity = $severity)
|
|
75
|
+
AND ($sessionId IS NULL OR n.sessionId = $sessionId)
|
|
76
|
+
AND ($agentId IS NULL OR n.agentId = $agentId)
|
|
77
|
+
AND score >= $minSimilarity
|
|
78
|
+
|
|
79
|
+
OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
|
|
80
|
+
WHERE NOT codeNode:SessionNote
|
|
81
|
+
AND NOT codeNode:SessionBookmark
|
|
82
|
+
AND NOT codeNode:Pheromone
|
|
83
|
+
AND NOT codeNode:SwarmTask
|
|
84
|
+
|
|
85
|
+
RETURN
|
|
86
|
+
n.id AS id,
|
|
87
|
+
n.topic AS topic,
|
|
88
|
+
n.content AS content,
|
|
89
|
+
n.category AS category,
|
|
90
|
+
n.severity AS severity,
|
|
91
|
+
n.agentId AS agentId,
|
|
92
|
+
n.sessionId AS sessionId,
|
|
93
|
+
n.createdAt AS createdAt,
|
|
94
|
+
n.expiresAt AS expiresAt,
|
|
95
|
+
score AS relevance,
|
|
96
|
+
collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
|
|
97
|
+
|
|
98
|
+
ORDER BY score DESC
|
|
99
|
+
LIMIT toInteger($limit)
|
|
100
|
+
`;
|
|
101
|
+
/**
|
|
102
|
+
* Filter-based (non-semantic) search for session notes
|
|
103
|
+
*/
|
|
104
|
+
const FILTER_SEARCH_NOTES_QUERY = `
|
|
105
|
+
MATCH (n:SessionNote)
|
|
106
|
+
WHERE n.projectId = $projectId
|
|
107
|
+
AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
|
|
108
|
+
AND ($category IS NULL OR n.category = $category)
|
|
109
|
+
AND ($severity IS NULL OR n.severity = $severity)
|
|
110
|
+
AND ($sessionId IS NULL OR n.sessionId = $sessionId)
|
|
111
|
+
AND ($agentId IS NULL OR n.agentId = $agentId)
|
|
112
|
+
|
|
113
|
+
OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
|
|
114
|
+
WHERE NOT codeNode:SessionNote
|
|
115
|
+
AND NOT codeNode:SessionBookmark
|
|
116
|
+
AND NOT codeNode:Pheromone
|
|
117
|
+
AND NOT codeNode:SwarmTask
|
|
118
|
+
|
|
119
|
+
RETURN
|
|
120
|
+
n.id AS id,
|
|
121
|
+
n.topic AS topic,
|
|
122
|
+
n.content AS content,
|
|
123
|
+
n.category AS category,
|
|
124
|
+
n.severity AS severity,
|
|
125
|
+
n.agentId AS agentId,
|
|
126
|
+
n.sessionId AS sessionId,
|
|
127
|
+
n.createdAt AS createdAt,
|
|
128
|
+
n.expiresAt AS expiresAt,
|
|
129
|
+
null AS relevance,
|
|
130
|
+
collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
|
|
131
|
+
|
|
132
|
+
ORDER BY n.createdAt DESC
|
|
133
|
+
LIMIT toInteger($limit)
|
|
134
|
+
`;
|
|
135
|
+
/**
|
|
136
|
+
* Generate a unique note ID
|
|
137
|
+
*/
|
|
138
|
+
const generateNoteId = () => {
|
|
139
|
+
return `note_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
140
|
+
};
|
|
141
|
+
export const createSaveSessionNoteTool = (server) => {
|
|
142
|
+
server.registerTool(TOOL_NAMES.saveSessionNote, {
|
|
143
|
+
title: TOOL_METADATA[TOOL_NAMES.saveSessionNote].title,
|
|
144
|
+
description: TOOL_METADATA[TOOL_NAMES.saveSessionNote].description,
|
|
145
|
+
inputSchema: {
|
|
146
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
147
|
+
sessionId: z.string().describe('Session identifier (e.g., conversation ID or session name)'),
|
|
148
|
+
agentId: z.string().describe('Agent identifier that is saving the note'),
|
|
149
|
+
topic: z.string().min(3).max(100).describe('Short topic label for the note (3-100 characters)'),
|
|
150
|
+
content: z.string().min(10).describe('Full observation text (minimum 10 characters)'),
|
|
151
|
+
category: z.enum(NOTE_CATEGORIES).describe('Category: architectural, bug, insight, decision, risk, or todo'),
|
|
152
|
+
severity: z
|
|
153
|
+
.enum(NOTE_SEVERITIES)
|
|
154
|
+
.optional()
|
|
155
|
+
.default('info')
|
|
156
|
+
.describe('Severity level: info (default), warning, or critical'),
|
|
157
|
+
aboutNodeIds: z
|
|
158
|
+
.array(z.string())
|
|
159
|
+
.optional()
|
|
160
|
+
.default([])
|
|
161
|
+
.describe('Code node IDs this note is about (links to graph nodes via [:ABOUT])'),
|
|
162
|
+
expiresInHours: z
|
|
163
|
+
.number()
|
|
164
|
+
.positive()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('Auto-expire after N hours. Omit for a permanent note.'),
|
|
167
|
+
},
|
|
168
|
+
}, async ({ projectId, sessionId, agentId, topic, content, category, severity = 'info', aboutNodeIds = [], expiresInHours, }) => {
|
|
169
|
+
const neo4jService = new Neo4jService();
|
|
170
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
171
|
+
if (!projectResult.success) {
|
|
172
|
+
await neo4jService.close();
|
|
173
|
+
return projectResult.error;
|
|
174
|
+
}
|
|
175
|
+
const resolvedProjectId = projectResult.projectId;
|
|
176
|
+
try {
|
|
177
|
+
const noteId = generateNoteId();
|
|
178
|
+
const expiresAt = expiresInHours != null ? Date.now() + expiresInHours * 3600 * 1000 : null;
|
|
179
|
+
const createResult = await neo4jService.run(CREATE_SESSION_NOTE_QUERY, {
|
|
180
|
+
noteId,
|
|
181
|
+
projectId: resolvedProjectId,
|
|
182
|
+
sessionId,
|
|
183
|
+
agentId,
|
|
184
|
+
topic,
|
|
185
|
+
content,
|
|
186
|
+
category,
|
|
187
|
+
severity,
|
|
188
|
+
aboutNodeIds,
|
|
189
|
+
expiresAt,
|
|
190
|
+
});
|
|
191
|
+
if (createResult.length === 0) {
|
|
192
|
+
return createErrorResponse('Failed to create session note.');
|
|
193
|
+
}
|
|
194
|
+
// Ensure vector index exists (idempotent — IF NOT EXISTS)
|
|
195
|
+
let hasEmbedding = false;
|
|
196
|
+
try {
|
|
197
|
+
await neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX);
|
|
198
|
+
const embeddingsService = new EmbeddingsService();
|
|
199
|
+
const embeddingText = `${topic}\n\n${content}`;
|
|
200
|
+
const embedding = await embeddingsService.embedText(embeddingText);
|
|
201
|
+
await neo4jService.run(SET_NOTE_EMBEDDING_QUERY, {
|
|
202
|
+
noteId,
|
|
203
|
+
projectId: resolvedProjectId,
|
|
204
|
+
embedding,
|
|
205
|
+
});
|
|
206
|
+
hasEmbedding = true;
|
|
207
|
+
}
|
|
208
|
+
catch (embErr) {
|
|
209
|
+
await debugLog('Session note embedding failed (non-fatal)', { error: String(embErr), noteId });
|
|
210
|
+
}
|
|
211
|
+
return createSuccessResponse(JSON.stringify({
|
|
212
|
+
success: true,
|
|
213
|
+
noteId,
|
|
214
|
+
topic,
|
|
215
|
+
category,
|
|
216
|
+
severity,
|
|
217
|
+
hasEmbedding,
|
|
218
|
+
expiresAt: expiresAt != null ? new Date(expiresAt).toISOString() : null,
|
|
219
|
+
projectId: resolvedProjectId,
|
|
220
|
+
sessionId,
|
|
221
|
+
agentId,
|
|
222
|
+
}, null, 2));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
await debugLog('Save session note error', { error: String(error) });
|
|
226
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
await neo4jService.close();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
export const createRecallSessionNotesTool = (server) => {
|
|
234
|
+
server.registerTool(TOOL_NAMES.recallSessionNotes, {
|
|
235
|
+
title: TOOL_METADATA[TOOL_NAMES.recallSessionNotes].title,
|
|
236
|
+
description: TOOL_METADATA[TOOL_NAMES.recallSessionNotes].description,
|
|
237
|
+
inputSchema: {
|
|
238
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
239
|
+
query: z
|
|
240
|
+
.string()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe('Natural language search query. When provided, triggers semantic vector search.'),
|
|
243
|
+
category: z
|
|
244
|
+
.enum(NOTE_CATEGORIES)
|
|
245
|
+
.optional()
|
|
246
|
+
.describe('Filter by category: architectural, bug, insight, decision, risk, todo'),
|
|
247
|
+
severity: z.enum(NOTE_SEVERITIES).optional().describe('Filter by severity: info, warning, critical'),
|
|
248
|
+
sessionId: z.string().optional().describe('Filter by session ID'),
|
|
249
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
250
|
+
limit: z
|
|
251
|
+
.number()
|
|
252
|
+
.int()
|
|
253
|
+
.min(1)
|
|
254
|
+
.max(50)
|
|
255
|
+
.optional()
|
|
256
|
+
.default(10)
|
|
257
|
+
.describe('Maximum number of notes to return (default: 10, max: 50)'),
|
|
258
|
+
minSimilarity: z
|
|
259
|
+
.number()
|
|
260
|
+
.min(0)
|
|
261
|
+
.max(1)
|
|
262
|
+
.optional()
|
|
263
|
+
.default(0.3)
|
|
264
|
+
.describe('Minimum similarity score for vector search (0.0-1.0, default: 0.3)'),
|
|
265
|
+
},
|
|
266
|
+
}, async ({ projectId, query, category, severity, sessionId, agentId, limit = 10, minSimilarity = 0.3 }) => {
|
|
267
|
+
const neo4jService = new Neo4jService();
|
|
268
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
269
|
+
if (!projectResult.success) {
|
|
270
|
+
await neo4jService.close();
|
|
271
|
+
return projectResult.error;
|
|
272
|
+
}
|
|
273
|
+
const resolvedProjectId = projectResult.projectId;
|
|
274
|
+
try {
|
|
275
|
+
let rawNotes;
|
|
276
|
+
if (query) {
|
|
277
|
+
// Semantic search mode
|
|
278
|
+
const embeddingsService = new EmbeddingsService();
|
|
279
|
+
const queryEmbedding = await embeddingsService.embedText(query);
|
|
280
|
+
rawNotes = await neo4jService.run(VECTOR_SEARCH_NOTES_QUERY, {
|
|
281
|
+
projectId: resolvedProjectId,
|
|
282
|
+
queryEmbedding,
|
|
283
|
+
limit: Math.floor(limit),
|
|
284
|
+
minSimilarity,
|
|
285
|
+
category: category ?? null,
|
|
286
|
+
severity: severity ?? null,
|
|
287
|
+
sessionId: sessionId ?? null,
|
|
288
|
+
agentId: agentId ?? null,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Filter-based search mode
|
|
293
|
+
rawNotes = await neo4jService.run(FILTER_SEARCH_NOTES_QUERY, {
|
|
294
|
+
projectId: resolvedProjectId,
|
|
295
|
+
limit: Math.floor(limit),
|
|
296
|
+
category: category ?? null,
|
|
297
|
+
severity: severity ?? null,
|
|
298
|
+
sessionId: sessionId ?? null,
|
|
299
|
+
agentId: agentId ?? null,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const notes = rawNotes.map((row) => {
|
|
303
|
+
const createdAt = typeof row.createdAt === 'object' && row.createdAt?.toNumber ? row.createdAt.toNumber() : row.createdAt;
|
|
304
|
+
const expiresAt = typeof row.expiresAt === 'object' && row.expiresAt?.toNumber ? row.expiresAt.toNumber() : row.expiresAt;
|
|
305
|
+
// Filter out null entries from optional ABOUT matches
|
|
306
|
+
const aboutNodes = (row.aboutNodes ?? []).filter((n) => n?.id != null);
|
|
307
|
+
return {
|
|
308
|
+
id: row.id,
|
|
309
|
+
topic: row.topic,
|
|
310
|
+
content: row.content,
|
|
311
|
+
category: row.category,
|
|
312
|
+
severity: row.severity,
|
|
313
|
+
relevance: row.relevance != null ? Math.round(row.relevance * 1000) / 1000 : null,
|
|
314
|
+
agentId: row.agentId,
|
|
315
|
+
sessionId: row.sessionId,
|
|
316
|
+
createdAt,
|
|
317
|
+
expiresAt,
|
|
318
|
+
aboutNodes,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
return createSuccessResponse(JSON.stringify({
|
|
322
|
+
count: notes.length,
|
|
323
|
+
projectId: resolvedProjectId,
|
|
324
|
+
searchMode: query ? 'semantic' : 'filter',
|
|
325
|
+
filters: {
|
|
326
|
+
query: query ?? null,
|
|
327
|
+
category: category ?? null,
|
|
328
|
+
severity: severity ?? null,
|
|
329
|
+
sessionId: sessionId ?? null,
|
|
330
|
+
agentId: agentId ?? null,
|
|
331
|
+
},
|
|
332
|
+
notes,
|
|
333
|
+
}, null, 2));
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
await debugLog('Recall session notes error', { error: String(error) });
|
|
337
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
await neo4jService.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
};
|
|
@@ -261,12 +261,9 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
261
261
|
.describe('Action: claim_and_start (RECOMMENDED: atomic claim+start), claim (reserve only), ' +
|
|
262
262
|
'start (begin work on claimed task), release (give up task), ' +
|
|
263
263
|
'abandon (release with tracking), force_start (recover from stuck claimed state)'),
|
|
264
|
-
releaseReason: z
|
|
265
|
-
.string()
|
|
266
|
-
.optional()
|
|
267
|
-
.describe('Reason for releasing/abandoning the task'),
|
|
264
|
+
releaseReason: z.string().optional().describe('Reason for releasing/abandoning the task'),
|
|
268
265
|
},
|
|
269
|
-
}, async ({ projectId, swarmId, agentId, taskId, types, minPriority, action = 'claim_and_start', releaseReason
|
|
266
|
+
}, async ({ projectId, swarmId, agentId, taskId, types, minPriority, action = 'claim_and_start', releaseReason }) => {
|
|
270
267
|
const neo4jService = new Neo4jService();
|
|
271
268
|
// Resolve project ID
|
|
272
269
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -323,9 +320,7 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
323
320
|
? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
|
|
324
321
|
: 'Task not found.'));
|
|
325
322
|
}
|
|
326
|
-
const abandonCount = typeof result[0].abandonCount === 'object'
|
|
327
|
-
? result[0].abandonCount.toNumber()
|
|
328
|
-
: result[0].abandonCount;
|
|
323
|
+
const abandonCount = typeof result[0].abandonCount === 'object' ? result[0].abandonCount.toNumber() : result[0].abandonCount;
|
|
329
324
|
return createSuccessResponse(JSON.stringify({ action: 'abandoned', taskId: result[0].id, abandonCount }));
|
|
330
325
|
}
|
|
331
326
|
// Handle force_start action (recovery from stuck claimed state)
|
|
@@ -406,9 +401,7 @@ export const createSwarmClaimTaskTool = (server) => {
|
|
|
406
401
|
}
|
|
407
402
|
else {
|
|
408
403
|
// Auto-select highest priority available task with retry logic
|
|
409
|
-
const minPriorityScore = minPriority
|
|
410
|
-
? TASK_PRIORITIES[minPriority]
|
|
411
|
-
: null;
|
|
404
|
+
const minPriorityScore = minPriority ? TASK_PRIORITIES[minPriority] : null;
|
|
412
405
|
// Retry loop to handle race conditions
|
|
413
406
|
while (retryCount < MAX_CLAIM_RETRIES) {
|
|
414
407
|
result = await neo4jService.run(CLAIM_NEXT_TASK_QUERY, {
|
|
@@ -96,7 +96,7 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
96
96
|
.describe('Pheromone types to preserve (default: ["warning"])'),
|
|
97
97
|
dryRun: z.boolean().optional().default(false).describe('Preview what would be deleted without deleting'),
|
|
98
98
|
},
|
|
99
|
-
}, async ({ projectId, swarmId, agentId, all = false, includeTasks = true, keepTypes = ['warning'], dryRun = false }) => {
|
|
99
|
+
}, async ({ projectId, swarmId, agentId, all = false, includeTasks = true, keepTypes = ['warning'], dryRun = false, }) => {
|
|
100
100
|
const neo4jService = new Neo4jService();
|
|
101
101
|
// Resolve project ID
|
|
102
102
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -139,7 +139,8 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
139
139
|
if (swarmId && includeTasks) {
|
|
140
140
|
const taskResult = await neo4jService.run(COUNT_TASKS_BY_SWARM_QUERY, params);
|
|
141
141
|
taskCount = taskResult[0]?.count ?? 0;
|
|
142
|
-
taskCount =
|
|
142
|
+
taskCount =
|
|
143
|
+
typeof taskCount === 'object' && 'toNumber' in taskCount ? taskCount.toNumber() : taskCount;
|
|
143
144
|
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
144
145
|
}
|
|
145
146
|
return createSuccessResponse(JSON.stringify({
|
|
@@ -147,14 +148,18 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
147
148
|
dryRun: true,
|
|
148
149
|
mode,
|
|
149
150
|
pheromones: {
|
|
150
|
-
wouldDelete: typeof pheromoneCount === 'object' && 'toNumber' in pheromoneCount
|
|
151
|
+
wouldDelete: typeof pheromoneCount === 'object' && 'toNumber' in pheromoneCount
|
|
152
|
+
? pheromoneCount.toNumber()
|
|
153
|
+
: pheromoneCount,
|
|
151
154
|
agents: pheromoneResult[0]?.agents ?? [],
|
|
152
155
|
types: pheromoneResult[0]?.types ?? [],
|
|
153
156
|
},
|
|
154
|
-
tasks: swarmId && includeTasks
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
tasks: swarmId && includeTasks
|
|
158
|
+
? {
|
|
159
|
+
wouldDelete: taskCount,
|
|
160
|
+
statuses: taskStatuses,
|
|
161
|
+
}
|
|
162
|
+
: null,
|
|
158
163
|
keepTypes,
|
|
159
164
|
projectId: resolvedProjectId,
|
|
160
165
|
}));
|
|
@@ -168,21 +173,28 @@ export const createSwarmCleanupTool = (server) => {
|
|
|
168
173
|
if (swarmId && includeTasks) {
|
|
169
174
|
const taskResult = await neo4jService.run(CLEANUP_TASKS_BY_SWARM_QUERY, params);
|
|
170
175
|
tasksDeleted = taskResult[0]?.deleted ?? 0;
|
|
171
|
-
tasksDeleted =
|
|
176
|
+
tasksDeleted =
|
|
177
|
+
typeof tasksDeleted === 'object' && 'toNumber' in tasksDeleted
|
|
178
|
+
? tasksDeleted.toNumber()
|
|
179
|
+
: tasksDeleted;
|
|
172
180
|
taskStatuses = taskResult[0]?.statuses ?? [];
|
|
173
181
|
}
|
|
174
182
|
return createSuccessResponse(JSON.stringify({
|
|
175
183
|
success: true,
|
|
176
184
|
mode,
|
|
177
185
|
pheromones: {
|
|
178
|
-
deleted: typeof pheromonesDeleted === 'object' && 'toNumber' in pheromonesDeleted
|
|
186
|
+
deleted: typeof pheromonesDeleted === 'object' && 'toNumber' in pheromonesDeleted
|
|
187
|
+
? pheromonesDeleted.toNumber()
|
|
188
|
+
: pheromonesDeleted,
|
|
179
189
|
agents: pheromoneResult[0]?.agents ?? [],
|
|
180
190
|
types: pheromoneResult[0]?.types ?? [],
|
|
181
191
|
},
|
|
182
|
-
tasks: swarmId && includeTasks
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
tasks: swarmId && includeTasks
|
|
193
|
+
? {
|
|
194
|
+
deleted: tasksDeleted,
|
|
195
|
+
statuses: taskStatuses,
|
|
196
|
+
}
|
|
197
|
+
: null,
|
|
186
198
|
keepTypes,
|
|
187
199
|
projectId: resolvedProjectId,
|
|
188
200
|
message: swarmId && includeTasks
|
|
@@ -249,45 +249,20 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
249
249
|
action: z
|
|
250
250
|
.enum(['complete', 'fail', 'request_review', 'approve', 'reject', 'retry'])
|
|
251
251
|
.describe('Action to take on the task'),
|
|
252
|
-
summary: z
|
|
253
|
-
.string()
|
|
254
|
-
.optional()
|
|
255
|
-
.describe('Summary of what was done (required for complete/request_review)'),
|
|
252
|
+
summary: z.string().optional().describe('Summary of what was done (required for complete/request_review)'),
|
|
256
253
|
artifacts: z
|
|
257
254
|
.record(z.unknown())
|
|
258
255
|
.optional()
|
|
259
256
|
.describe('Artifacts produced: { files: [], commits: [], pullRequests: [], notes: string }'),
|
|
260
|
-
filesChanged: z
|
|
261
|
-
.array(z.string())
|
|
262
|
-
.optional()
|
|
263
|
-
.describe('List of files that were modified'),
|
|
257
|
+
filesChanged: z.array(z.string()).optional().describe('List of files that were modified'),
|
|
264
258
|
linesAdded: z.number().int().optional().describe('Number of lines added'),
|
|
265
259
|
linesRemoved: z.number().int().optional().describe('Number of lines removed'),
|
|
266
|
-
reason: z
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.optional()
|
|
273
|
-
.describe('Technical error details for debugging'),
|
|
274
|
-
retryable: z
|
|
275
|
-
.boolean()
|
|
276
|
-
.optional()
|
|
277
|
-
.default(true)
|
|
278
|
-
.describe('Whether the task can be retried after failure'),
|
|
279
|
-
reviewNotes: z
|
|
280
|
-
.string()
|
|
281
|
-
.optional()
|
|
282
|
-
.describe('Notes for the reviewer (for request_review)'),
|
|
283
|
-
reviewerId: z
|
|
284
|
-
.string()
|
|
285
|
-
.optional()
|
|
286
|
-
.describe('ID of the reviewer (required for approve/reject)'),
|
|
287
|
-
notes: z
|
|
288
|
-
.string()
|
|
289
|
-
.optional()
|
|
290
|
-
.describe('Approval/rejection notes'),
|
|
260
|
+
reason: z.string().optional().describe('Reason for failure (required if action=fail)'),
|
|
261
|
+
errorDetails: z.string().optional().describe('Technical error details for debugging'),
|
|
262
|
+
retryable: z.boolean().optional().default(true).describe('Whether the task can be retried after failure'),
|
|
263
|
+
reviewNotes: z.string().optional().describe('Notes for the reviewer (for request_review)'),
|
|
264
|
+
reviewerId: z.string().optional().describe('ID of the reviewer (required for approve/reject)'),
|
|
265
|
+
notes: z.string().optional().describe('Approval/rejection notes'),
|
|
291
266
|
markAsFailed: z
|
|
292
267
|
.boolean()
|
|
293
268
|
.optional()
|
|
@@ -14,6 +14,7 @@ export const PHEROMONE_CONFIG = {
|
|
|
14
14
|
blocked: { halfLife: 5 * 60 * 1000, description: 'Stuck' },
|
|
15
15
|
proposal: { halfLife: 60 * 60 * 1000, description: 'Awaiting approval' },
|
|
16
16
|
needs_review: { halfLife: 30 * 60 * 1000, description: 'Review requested' },
|
|
17
|
+
session_context: { halfLife: 8 * 60 * 60 * 1000, description: 'Session working context marker' },
|
|
17
18
|
};
|
|
18
19
|
/**
|
|
19
20
|
* Task status values for the blackboard task queue
|
|
@@ -76,7 +77,7 @@ export const WORKFLOW_STATES = ['exploring', 'claiming', 'modifying', 'completed
|
|
|
76
77
|
/**
|
|
77
78
|
* Flags can coexist with workflow states.
|
|
78
79
|
*/
|
|
79
|
-
export const FLAG_TYPES = ['warning', 'proposal', 'needs_review'];
|
|
80
|
+
export const FLAG_TYPES = ['warning', 'proposal', 'needs_review', 'session_context'];
|
|
80
81
|
// ============================================================================
|
|
81
82
|
// ORCHESTRATOR CONSTANTS
|
|
82
83
|
// ============================================================================
|
|
@@ -167,10 +167,7 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
167
167
|
.array(z.enum(TASK_STATUSES))
|
|
168
168
|
.optional()
|
|
169
169
|
.describe('Filter by task statuses (e.g., ["available", "in_progress"])'),
|
|
170
|
-
types: z
|
|
171
|
-
.array(z.enum(TASK_TYPES))
|
|
172
|
-
.optional()
|
|
173
|
-
.describe('Filter by task types (e.g., ["implement", "fix"])'),
|
|
170
|
+
types: z.array(z.enum(TASK_TYPES)).optional().describe('Filter by task types (e.g., ["implement", "fix"])'),
|
|
174
171
|
claimedBy: z.string().optional().describe('Filter tasks claimed by a specific agent'),
|
|
175
172
|
createdBy: z.string().optional().describe('Filter tasks created by a specific agent'),
|
|
176
173
|
minPriority: z
|
|
@@ -241,7 +238,7 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
241
238
|
}
|
|
242
239
|
}
|
|
243
240
|
// Convert Neo4j integers
|
|
244
|
-
const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
|
|
241
|
+
const convertTimestamp = (ts) => (typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts);
|
|
245
242
|
task.createdAt = convertTimestamp(task.createdAt);
|
|
246
243
|
task.updatedAt = convertTimestamp(task.updatedAt);
|
|
247
244
|
task.claimedAt = convertTimestamp(task.claimedAt);
|
|
@@ -250,9 +247,7 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
250
247
|
return createSuccessResponse(JSON.stringify({ success: true, task }));
|
|
251
248
|
}
|
|
252
249
|
// Get list of tasks
|
|
253
|
-
const minPriorityScore = minPriority
|
|
254
|
-
? TASK_PRIORITIES[minPriority]
|
|
255
|
-
: null;
|
|
250
|
+
const minPriorityScore = minPriority ? TASK_PRIORITIES[minPriority] : null;
|
|
256
251
|
const tasksResult = await neo4jService.run(GET_TASKS_QUERY, {
|
|
257
252
|
projectId: resolvedProjectId,
|
|
258
253
|
swarmId: swarmId || null,
|
|
@@ -265,7 +260,7 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
265
260
|
limit: Math.floor(limit),
|
|
266
261
|
skip: Math.floor(skip),
|
|
267
262
|
});
|
|
268
|
-
const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
|
|
263
|
+
const convertTimestamp = (ts) => (typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts);
|
|
269
264
|
const tasks = tasksResult.map((t) => {
|
|
270
265
|
// Parse metadata if present
|
|
271
266
|
let metadata = t.metadata;
|
|
@@ -379,9 +374,7 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
379
374
|
status: w.type === 'modifying' ? 'working' : 'claiming',
|
|
380
375
|
lastActivity: typeof w.lastActivity === 'object' ? w.lastActivity.toNumber() : w.lastActivity,
|
|
381
376
|
nodesBeingWorked: typeof w.nodeCount === 'object' ? w.nodeCount.toNumber() : w.nodeCount,
|
|
382
|
-
minutesSinceActivity: typeof w.minutesSinceActivity === 'object'
|
|
383
|
-
? w.minutesSinceActivity.toNumber()
|
|
384
|
-
: w.minutesSinceActivity,
|
|
377
|
+
minutesSinceActivity: typeof w.minutesSinceActivity === 'object' ? w.minutesSinceActivity.toNumber() : w.minutesSinceActivity,
|
|
385
378
|
}));
|
|
386
379
|
}
|
|
387
380
|
// Include dependency graph if requested
|
|
@@ -14,7 +14,7 @@ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
|
14
14
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
15
15
|
import { TaskDecompositionHandler, } from '../handlers/task-decomposition.handler.js';
|
|
16
16
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
17
|
-
import { TASK_PRIORITIES, generateSwarmId, ORCHESTRATOR_CONFIG, getHalfLife
|
|
17
|
+
import { TASK_PRIORITIES, generateSwarmId, ORCHESTRATOR_CONFIG, getHalfLife } from './swarm-constants.js';
|
|
18
18
|
/**
|
|
19
19
|
* Query to search for nodes matching the task description
|
|
20
20
|
*/
|
|
@@ -415,14 +415,16 @@ swarm_complete_task({
|
|
|
415
415
|
## IF STUCK
|
|
416
416
|
swarm_complete_task({ ..., action: "fail", reason: "<WHY>", retryable: true })
|
|
417
417
|
Then claim another task.`;
|
|
418
|
-
const taskCalls = agentIds
|
|
418
|
+
const taskCalls = agentIds
|
|
419
|
+
.map((agentId) => {
|
|
419
420
|
const prompt = workerPrompt.replace(/\{AGENT_ID\}/g, agentId);
|
|
420
421
|
return `Task({
|
|
421
422
|
subagent_type: "general-purpose",
|
|
422
423
|
run_in_background: false,
|
|
423
424
|
prompt: \`${prompt}\`
|
|
424
425
|
})`;
|
|
425
|
-
})
|
|
426
|
+
})
|
|
427
|
+
.join('\n\n');
|
|
426
428
|
return `
|
|
427
429
|
## Worker Agent Instructions
|
|
428
430
|
|
|
@@ -41,11 +41,13 @@ const CREATE_PHEROMONE_QUERY = `
|
|
|
41
41
|
p.intensity = $intensity,
|
|
42
42
|
p.timestamp = timestamp(),
|
|
43
43
|
p.data = $data,
|
|
44
|
-
p.halfLife = $halfLife
|
|
44
|
+
p.halfLife = $halfLife,
|
|
45
|
+
p.sessionId = $sessionId
|
|
45
46
|
ON MATCH SET
|
|
46
47
|
p.intensity = $intensity,
|
|
47
48
|
p.timestamp = timestamp(),
|
|
48
|
-
p.data = $data
|
|
49
|
+
p.data = $data,
|
|
50
|
+
p.sessionId = COALESCE($sessionId, p.sessionId)
|
|
49
51
|
|
|
50
52
|
// Create relationship to target node if it exists
|
|
51
53
|
WITH p, target
|
|
@@ -73,7 +75,7 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
73
75
|
nodeId: z.string().describe('The code node ID to mark with a pheromone'),
|
|
74
76
|
type: z
|
|
75
77
|
.enum(PHEROMONE_TYPES)
|
|
76
|
-
.describe('Type of pheromone: exploring (browsing), modifying (active work), claiming (ownership), completed (done), warning (danger), blocked (stuck), proposal (awaiting approval), needs_review (review request)'),
|
|
78
|
+
.describe('Type of pheromone: exploring (browsing), modifying (active work), claiming (ownership), completed (done), warning (danger), blocked (stuck), proposal (awaiting approval), needs_review (review request), session_context (session working set)'),
|
|
77
79
|
intensity: z
|
|
78
80
|
.number()
|
|
79
81
|
.min(0)
|
|
@@ -83,6 +85,10 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
83
85
|
.describe('Pheromone intensity from 0.0 to 1.0 (default: 1.0)'),
|
|
84
86
|
agentId: z.string().describe('Unique identifier for the agent leaving the pheromone'),
|
|
85
87
|
swarmId: z.string().describe('Swarm ID for grouping related agents (e.g., "swarm_xyz")'),
|
|
88
|
+
sessionId: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Session identifier for cross-session recovery (e.g., conversation ID)'),
|
|
86
92
|
data: z
|
|
87
93
|
.record(z.unknown())
|
|
88
94
|
.optional()
|
|
@@ -93,7 +99,7 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
93
99
|
.default(false)
|
|
94
100
|
.describe('If true, removes the pheromone instead of creating/updating it'),
|
|
95
101
|
},
|
|
96
|
-
}, async ({ projectId, nodeId, type, intensity = 1.0, agentId, swarmId, data, remove = false }) => {
|
|
102
|
+
}, async ({ projectId, nodeId, type, intensity = 1.0, agentId, swarmId, sessionId, data, remove = false }) => {
|
|
97
103
|
const neo4jService = new Neo4jService();
|
|
98
104
|
// Resolve project ID
|
|
99
105
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -159,6 +165,7 @@ export const createSwarmPheromoneTool = (server) => {
|
|
|
159
165
|
intensity,
|
|
160
166
|
agentId,
|
|
161
167
|
swarmId,
|
|
168
|
+
sessionId: sessionId ?? null,
|
|
162
169
|
data: dataJson,
|
|
163
170
|
halfLife,
|
|
164
171
|
});
|