code-graph-context 2.7.0 → 2.9.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.
@@ -1,471 +0,0 @@
1
- /**
2
- * Swarm Orchestrate Tool
3
- * Orchestrates multiple agents to tackle complex, multi-file code tasks in parallel
4
- *
5
- * This is the main entry point for swarm-based task execution. It:
6
- * 1. Analyzes the task using semantic search and impact analysis
7
- * 2. Decomposes the task into atomic, dependency-ordered SwarmTasks
8
- * 3. Creates tasks on the blackboard for worker agents
9
- * 4. Returns execution plan for agents to claim and execute
10
- */
11
- import { z } from 'zod';
12
- import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
13
- import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
14
- import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
15
- import { TaskDecompositionHandler, } from '../handlers/task-decomposition.handler.js';
16
- import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
17
- import { TASK_PRIORITIES, generateSwarmId, ORCHESTRATOR_CONFIG, getHalfLife } from './swarm-constants.js';
18
- /**
19
- * Query to search for nodes matching the task description
20
- */
21
- const SEMANTIC_SEARCH_QUERY = `
22
- CALL db.index.vector.queryNodes('embedded_nodes_idx', toInteger($limit), $embedding)
23
- YIELD node, score
24
- WHERE node.projectId = $projectId
25
- AND score >= $minSimilarity
26
- RETURN node.id AS id,
27
- node.name AS name,
28
- node.coreType AS coreType,
29
- node.semanticType AS semanticType,
30
- node.filePath AS filePath,
31
- substring(node.sourceCode, 0, 500) AS sourceCode,
32
- node.startLine AS startLine,
33
- node.endLine AS endLine,
34
- score
35
- ORDER BY score DESC
36
- LIMIT toInteger($limit)
37
- `;
38
- /**
39
- * Query to get impact analysis for a node
40
- */
41
- const IMPACT_QUERY = `
42
- MATCH (target)
43
- WHERE target.id = $nodeId AND target.projectId = $projectId
44
- OPTIONAL MATCH (dependent)-[r]->(target)
45
- WHERE dependent.projectId = $projectId
46
- AND NOT dependent:Pheromone
47
- AND NOT dependent:SwarmTask
48
- WITH target, collect(DISTINCT {
49
- nodeId: dependent.id,
50
- filePath: dependent.filePath,
51
- relType: type(r)
52
- }) AS dependents
53
- RETURN target.id AS nodeId,
54
- size(dependents) AS dependentCount,
55
- [d IN dependents | d.filePath] AS affectedFiles,
56
- CASE
57
- WHEN size(dependents) >= 20 THEN 'CRITICAL'
58
- WHEN size(dependents) >= 10 THEN 'HIGH'
59
- WHEN size(dependents) >= 5 THEN 'MEDIUM'
60
- ELSE 'LOW'
61
- END AS riskLevel
62
- `;
63
- /**
64
- * Query to create a pheromone marker on a node
65
- */
66
- const CREATE_PHEROMONE_QUERY = `
67
- MATCH (target)
68
- WHERE target.id = $nodeId AND target.projectId = $projectId
69
- MERGE (p:Pheromone {
70
- nodeId: $nodeId,
71
- agentId: $agentId,
72
- type: $type,
73
- projectId: $projectId
74
- })
75
- ON CREATE SET
76
- p.id = randomUUID(),
77
- p.swarmId = $swarmId,
78
- p.intensity = $intensity,
79
- p.timestamp = timestamp(),
80
- p.halfLife = $halfLife,
81
- p.data = $data
82
- ON MATCH SET
83
- p.intensity = $intensity,
84
- p.timestamp = timestamp(),
85
- p.data = $data
86
- MERGE (p)-[:MARKS]->(target)
87
- RETURN p.nodeId AS nodeId
88
- `;
89
- /**
90
- * Query to get node IDs for a file path (fallback when decomposition has no nodeIds)
91
- * Uses ENDS WITH for flexible matching (handles absolute vs relative paths)
92
- */
93
- const GET_NODES_FOR_FILE_QUERY = `
94
- MATCH (n)
95
- WHERE (n.filePath = $filePath OR n.filePath ENDS WITH $filePath)
96
- AND n.projectId = $projectId
97
- AND n.id IS NOT NULL
98
- AND NOT n:Pheromone
99
- AND NOT n:SwarmTask
100
- RETURN n.id AS id, n.name AS name, n.coreType AS coreType
101
- ORDER BY n.startLine
102
- LIMIT 20
103
- `;
104
- /**
105
- * Query to create a SwarmTask node
106
- */
107
- const CREATE_TASK_QUERY = `
108
- CREATE (t:SwarmTask {
109
- id: $taskId,
110
- projectId: $projectId,
111
- swarmId: $swarmId,
112
- title: $title,
113
- description: $description,
114
- type: $type,
115
- priority: $priority,
116
- priorityScore: $priorityScore,
117
- status: $status,
118
- targetNodeIds: $targetNodeIds,
119
- targetFilePaths: $targetFilePaths,
120
- dependencies: $dependencies,
121
- createdBy: $createdBy,
122
- createdAt: timestamp(),
123
- updatedAt: timestamp(),
124
- metadata: $metadata
125
- })
126
-
127
- // Link to target code nodes if they exist
128
- WITH t
129
- OPTIONAL MATCH (target)
130
- WHERE target.id IN $targetNodeIds
131
- AND target.projectId = $projectId
132
- AND NOT target:SwarmTask
133
- AND NOT target:Pheromone
134
- WITH t, collect(DISTINCT target) as targets
135
- FOREACH (target IN targets | MERGE (t)-[:TARGETS]->(target))
136
-
137
- // Link to dependency tasks if they exist
138
- WITH t
139
- OPTIONAL MATCH (dep:SwarmTask)
140
- WHERE dep.id IN $dependencies
141
- AND dep.projectId = $projectId
142
- WITH t, collect(DISTINCT dep) as deps
143
- FOREACH (dep IN deps | MERGE (t)-[:DEPENDS_ON]->(dep))
144
-
145
- RETURN t.id AS id
146
- `;
147
- export const createSwarmOrchestrateTool = (server) => {
148
- const embeddingsService = new EmbeddingsService();
149
- const taskDecomposer = new TaskDecompositionHandler();
150
- server.registerTool(TOOL_NAMES.swarmOrchestrate, {
151
- title: TOOL_METADATA[TOOL_NAMES.swarmOrchestrate].title,
152
- description: TOOL_METADATA[TOOL_NAMES.swarmOrchestrate].description,
153
- inputSchema: {
154
- projectId: z.string().describe('Project ID, name, or path'),
155
- task: z.string().min(10).describe('Natural language description of the task to execute'),
156
- maxAgents: z
157
- .number()
158
- .int()
159
- .min(1)
160
- .max(ORCHESTRATOR_CONFIG.maxAgentsLimit)
161
- .optional()
162
- .default(ORCHESTRATOR_CONFIG.defaultMaxAgents)
163
- .describe(`Maximum concurrent worker agents (default: ${ORCHESTRATOR_CONFIG.defaultMaxAgents})`),
164
- dryRun: z
165
- .boolean()
166
- .optional()
167
- .default(false)
168
- .describe('If true, only plan without creating tasks (default: false)'),
169
- priority: z
170
- .enum(Object.keys(TASK_PRIORITIES))
171
- .optional()
172
- .default('normal')
173
- .describe('Overall priority level for tasks'),
174
- minSimilarity: z
175
- .number()
176
- .min(0.5)
177
- .max(1.0)
178
- .optional()
179
- .default(0.65)
180
- .describe('Minimum similarity score for semantic search (default: 0.65)'),
181
- maxNodes: z
182
- .number()
183
- .int()
184
- .min(1)
185
- .max(100)
186
- .optional()
187
- .default(50)
188
- .describe('Maximum nodes to consider from search (default: 50)'),
189
- },
190
- }, async ({ projectId, task, maxAgents = ORCHESTRATOR_CONFIG.defaultMaxAgents, dryRun = false, priority = 'normal', minSimilarity = 0.65, maxNodes = 50, }) => {
191
- const neo4jService = new Neo4jService();
192
- const swarmId = generateSwarmId();
193
- try {
194
- // Step 1: Resolve project ID
195
- const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
196
- if (!projectResult.success) {
197
- await neo4jService.close();
198
- return projectResult.error;
199
- }
200
- const resolvedProjectId = projectResult.projectId;
201
- // Step 2: Semantic search to find affected nodes
202
- let embedding;
203
- try {
204
- embedding = await embeddingsService.embedText(task);
205
- }
206
- catch (error) {
207
- return createErrorResponse(`Failed to generate embedding for task description: ${error}`);
208
- }
209
- const searchResults = await neo4jService.run(SEMANTIC_SEARCH_QUERY, {
210
- projectId: resolvedProjectId,
211
- embedding,
212
- minSimilarity,
213
- limit: Math.floor(maxNodes),
214
- });
215
- if (searchResults.length === 0) {
216
- return createErrorResponse(`No code found matching task: "${task}". Try rephrasing or use search_codebase to explore the codebase first.`);
217
- }
218
- const affectedNodes = searchResults.map((r) => ({
219
- id: r.id,
220
- name: r.name,
221
- coreType: r.coreType,
222
- semanticType: r.semanticType,
223
- filePath: r.filePath,
224
- sourceCode: r.sourceCode,
225
- startLine: typeof r.startLine === 'object' ? r.startLine.toNumber() : r.startLine,
226
- endLine: typeof r.endLine === 'object' ? r.endLine.toNumber() : r.endLine,
227
- }));
228
- // Step 3: Run impact analysis on each node
229
- const impactMap = new Map();
230
- for (const node of affectedNodes) {
231
- const impactResult = await neo4jService.run(IMPACT_QUERY, {
232
- nodeId: node.id,
233
- projectId: resolvedProjectId,
234
- });
235
- if (impactResult.length > 0) {
236
- const impact = impactResult[0];
237
- impactMap.set(node.id, {
238
- nodeId: node.id,
239
- riskLevel: impact.riskLevel,
240
- directDependents: {
241
- count: typeof impact.dependentCount === 'object'
242
- ? impact.dependentCount.toNumber()
243
- : impact.dependentCount,
244
- byType: {},
245
- },
246
- transitiveDependents: { count: 0 },
247
- affectedFiles: impact.affectedFiles ?? [],
248
- });
249
- }
250
- }
251
- // Step 4: Decompose task into atomic tasks
252
- const decomposition = await taskDecomposer.decomposeTask(task, affectedNodes, impactMap, priority);
253
- if (decomposition.tasks.length === 0) {
254
- return createErrorResponse('Task decomposition produced no actionable tasks');
255
- }
256
- // Step 5: Create SwarmTasks on the blackboard (unless dry run)
257
- if (!dryRun) {
258
- for (const atomicTask of decomposition.tasks) {
259
- // Determine initial status based on dependencies
260
- const hasUnmetDeps = atomicTask.dependencies.length > 0;
261
- const initialStatus = hasUnmetDeps ? 'blocked' : 'available';
262
- // Filter out null/undefined nodeIds, then fallback to file query if empty
263
- let targetNodeIds = (atomicTask.nodeIds || []).filter((id) => typeof id === 'string' && id.length > 0);
264
- if (targetNodeIds.length === 0 && atomicTask.filePath) {
265
- const fileNodes = await neo4jService.run(GET_NODES_FOR_FILE_QUERY, {
266
- filePath: atomicTask.filePath,
267
- projectId: resolvedProjectId,
268
- });
269
- if (fileNodes.length > 0) {
270
- targetNodeIds = fileNodes.map((n) => n.id).filter(Boolean);
271
- }
272
- }
273
- await neo4jService.run(CREATE_TASK_QUERY, {
274
- taskId: atomicTask.id,
275
- projectId: resolvedProjectId,
276
- swarmId,
277
- title: atomicTask.title,
278
- description: atomicTask.description,
279
- type: atomicTask.type,
280
- priority: atomicTask.priority,
281
- priorityScore: atomicTask.priorityScore,
282
- status: initialStatus,
283
- targetNodeIds,
284
- targetFilePaths: [atomicTask.filePath],
285
- dependencies: atomicTask.dependencies,
286
- createdBy: 'orchestrator',
287
- metadata: JSON.stringify(atomicTask.metadata ?? {}),
288
- });
289
- // Update the atomicTask.nodeIds for pheromone creation below
290
- atomicTask.nodeIds = targetNodeIds;
291
- }
292
- // Step 5b: Leave "proposal" pheromones on all target nodes
293
- // This signals to other agents that work is planned for these nodes
294
- const uniqueNodeIds = new Set();
295
- for (const atomicTask of decomposition.tasks) {
296
- for (const nodeId of atomicTask.nodeIds) {
297
- uniqueNodeIds.add(nodeId);
298
- }
299
- }
300
- for (const nodeId of uniqueNodeIds) {
301
- await neo4jService.run(CREATE_PHEROMONE_QUERY, {
302
- nodeId,
303
- projectId: resolvedProjectId,
304
- agentId: 'orchestrator',
305
- swarmId,
306
- type: 'proposal',
307
- intensity: 1.0,
308
- halfLife: getHalfLife('proposal'),
309
- data: JSON.stringify({ task, swarmId }),
310
- });
311
- }
312
- }
313
- // Step 6: Generate worker instructions
314
- const workerInstructions = generateWorkerInstructions(swarmId, resolvedProjectId, maxAgents, decomposition.tasks.length);
315
- // Step 7: Build result
316
- const result = {
317
- swarmId,
318
- status: dryRun ? 'planning' : 'ready',
319
- plan: {
320
- totalTasks: decomposition.tasks.length,
321
- parallelizable: decomposition.summary.parallelizable,
322
- sequential: decomposition.summary.sequential,
323
- estimatedComplexity: decomposition.summary.estimatedComplexity,
324
- tasks: decomposition.tasks.map((t) => ({
325
- id: t.id,
326
- title: t.title,
327
- type: t.type,
328
- priority: t.priority,
329
- status: t.dependencies.length > 0 ? 'blocked' : 'available',
330
- dependencyCount: t.dependencies.length,
331
- targetFiles: [t.filePath],
332
- })),
333
- dependencyGraph: buildDependencyGraph(decomposition),
334
- },
335
- workerInstructions,
336
- message: dryRun
337
- ? `Dry run complete. ${decomposition.tasks.length} tasks planned but not created.`
338
- : `Swarm ready! ${decomposition.tasks.length} tasks created. ${decomposition.summary.parallelizable} can run in parallel.`,
339
- };
340
- return createSuccessResponse(JSON.stringify(result, null, 2));
341
- }
342
- catch (error) {
343
- await debugLog('Swarm orchestration error', { swarmId, error: String(error) });
344
- return createErrorResponse(error instanceof Error ? error : String(error));
345
- }
346
- finally {
347
- await neo4jService.close();
348
- }
349
- });
350
- };
351
- /**
352
- * Generate instructions for spawning worker agents
353
- */
354
- function generateWorkerInstructions(swarmId, projectId, maxAgents, taskCount) {
355
- const recommendedAgents = Math.min(maxAgents, Math.ceil(taskCount / 2), taskCount);
356
- // Generate unique agent IDs for each worker
357
- const agentIds = Array.from({ length: recommendedAgents }, (_, i) => `${swarmId}_worker_${i + 1}`);
358
- const workerPrompt = `You are a swarm worker agent with access to a code graph.
359
- - Agent ID: {AGENT_ID}
360
- - Swarm ID: ${swarmId}
361
- - Project: ${projectId}
362
-
363
- ## RULES
364
- 1. Use graph tools (traverse_from_node, search_codebase) for context
365
- 2. Tasks provide: targets (best), targetNodeIds (good), targetFilePaths (fallback)
366
- 3. Exit when swarm_claim_task returns "no_tasks"
367
-
368
- ## WORKFLOW
369
-
370
- ### Step 1: Claim a task
371
- swarm_claim_task({
372
- projectId: "${projectId}",
373
- swarmId: "${swarmId}",
374
- agentId: "{AGENT_ID}"
375
- })
376
- // Returns: { task: { id, targets: [{nodeId, name, filePath}], targetFilePaths, ... } }
377
- // If "no_tasks" → exit
378
-
379
- ### Step 2: Understand context via graph (USE NODE IDs!)
380
- // Priority: task.targets[0].nodeId > task.targetNodeIds[0] > search
381
- traverse_from_node({
382
- projectId: "${projectId}",
383
- nodeId: "<nodeId_FROM_task.targets>",
384
- maxDepth: 2
385
- })
386
-
387
- // Fallback if no nodeIds:
388
- search_codebase({ projectId: "${projectId}", query: "<TASK_DESCRIPTION>" })
389
-
390
- ### Step 3: Mark nodes you're analyzing/modifying
391
- swarm_pheromone({
392
- projectId: "${projectId}",
393
- nodeId: "<nodeId>",
394
- type: "modifying",
395
- agentId: "{AGENT_ID}",
396
- swarmId: "${swarmId}"
397
- })
398
-
399
- ### Step 4: Do the work
400
- - Read tool for full source
401
- - Edit tool for changes
402
-
403
- ### Step 5: Complete the task
404
- swarm_complete_task({
405
- projectId: "${projectId}",
406
- taskId: "<TASK_ID>",
407
- agentId: "{AGENT_ID}",
408
- action: "complete",
409
- summary: "<WHAT_YOU_DID>",
410
- filesChanged: ["<FILES>"]
411
- })
412
-
413
- ### Step 6: Loop to Step 1
414
-
415
- ## IF STUCK
416
- swarm_complete_task({ ..., action: "fail", reason: "<WHY>", retryable: true })
417
- Then claim another task.`;
418
- const taskCalls = agentIds
419
- .map((agentId) => {
420
- const prompt = workerPrompt.replace(/\{AGENT_ID\}/g, agentId);
421
- return `Task({
422
- subagent_type: "general-purpose",
423
- run_in_background: false,
424
- prompt: \`${prompt}\`
425
- })`;
426
- })
427
- .join('\n\n');
428
- return `
429
- ## Worker Agent Instructions
430
-
431
- Spawn ${recommendedAgents} worker agent(s) IN PARALLEL (all Task calls in ONE message):
432
-
433
- \`\`\`javascript
434
- ${taskCalls}
435
- \`\`\`
436
-
437
- **CRITICAL:** Include ALL ${recommendedAgents} Task calls in a single message to run them in parallel.
438
-
439
- ## Monitoring Progress
440
-
441
- Check swarm progress:
442
- \`\`\`javascript
443
- swarm_get_tasks({
444
- projectId: "${projectId}",
445
- swarmId: "${swarmId}",
446
- includeStats: true
447
- })
448
- \`\`\`
449
-
450
- ## Cleanup (after all workers complete)
451
-
452
- \`\`\`javascript
453
- swarm_cleanup({
454
- projectId: "${projectId}",
455
- swarmId: "${swarmId}"
456
- })
457
- \`\`\`
458
- `;
459
- }
460
- /**
461
- * Build dependency graph edges for visualization
462
- */
463
- function buildDependencyGraph(decomposition) {
464
- const edges = [];
465
- for (const task of decomposition.tasks) {
466
- for (const depId of task.dependencies) {
467
- edges.push({ from: depId, to: task.id });
468
- }
469
- }
470
- return edges;
471
- }