code-graph-context 2.6.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -22
- package/dist/core/utils/graph-factory.js +1 -1
- package/dist/mcp/constants.js +89 -7
- package/dist/mcp/handlers/graph-generator.handler.js +4 -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 +11 -3
- package/dist/mcp/tools/parse-typescript-project.tool.js +4 -3
- package/dist/mcp/tools/session-bookmark.tool.js +335 -0
- package/dist/mcp/tools/session-cleanup.tool.js +139 -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-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 +13 -1
- package/package.json +1 -1
- package/dist/mcp/tools/swarm-orchestrate.tool.js +0 -469
|
@@ -6,7 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
7
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
8
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
-
import { TASK_PRIORITIES, TASK_TYPES, generateTaskId
|
|
9
|
+
import { TASK_PRIORITIES, TASK_TYPES, generateTaskId } from './swarm-constants.js';
|
|
10
10
|
/**
|
|
11
11
|
* Query to get node IDs for a file path (fallback when no nodeIds provided)
|
|
12
12
|
* Uses ENDS WITH for flexible matching (handles absolute vs relative paths)
|
|
@@ -135,10 +135,7 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
135
135
|
.default([])
|
|
136
136
|
.describe('Task IDs that must be completed before this task can start'),
|
|
137
137
|
createdBy: z.string().describe('Agent ID or identifier of who created this task'),
|
|
138
|
-
metadata: z
|
|
139
|
-
.record(z.unknown())
|
|
140
|
-
.optional()
|
|
141
|
-
.describe('Additional metadata (context, acceptance criteria, etc.)'),
|
|
138
|
+
metadata: z.record(z.unknown()).optional().describe('Additional metadata (context, acceptance criteria, etc.)'),
|
|
142
139
|
},
|
|
143
140
|
}, async ({ projectId, swarmId, title, description, type = 'implement', priority = 'normal', targetNodeIds = [], targetFilePaths = [], dependencies = [], createdBy, metadata, }) => {
|
|
144
141
|
const neo4jService = new Neo4jService();
|
|
@@ -162,10 +159,7 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
162
159
|
projectId: resolvedProjectId,
|
|
163
160
|
});
|
|
164
161
|
if (fileNodes.length > 0) {
|
|
165
|
-
resolvedNodeIds = [
|
|
166
|
-
...resolvedNodeIds,
|
|
167
|
-
...fileNodes.map((n) => n.id).filter(Boolean),
|
|
168
|
-
];
|
|
162
|
+
resolvedNodeIds = [...resolvedNodeIds, ...fileNodes.map((n) => n.id).filter(Boolean)];
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
}
|
|
@@ -198,9 +192,7 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
198
192
|
});
|
|
199
193
|
if (depCheck.length > 0) {
|
|
200
194
|
dependencyStatus = {
|
|
201
|
-
totalDeps: typeof depCheck[0].totalDeps === 'object'
|
|
202
|
-
? depCheck[0].totalDeps.toNumber()
|
|
203
|
-
: depCheck[0].totalDeps,
|
|
195
|
+
totalDeps: typeof depCheck[0].totalDeps === 'object' ? depCheck[0].totalDeps.toNumber() : depCheck[0].totalDeps,
|
|
204
196
|
incompleteDeps: typeof depCheck[0].incompleteDeps === 'object'
|
|
205
197
|
? depCheck[0].incompleteDeps.toNumber()
|
|
206
198
|
: depCheck[0].incompleteDeps,
|
|
@@ -232,9 +224,7 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
232
224
|
targetFilePaths: task.targetFilePaths,
|
|
233
225
|
dependencies: task.dependencies,
|
|
234
226
|
createdBy: task.createdBy,
|
|
235
|
-
createdAt: typeof task.createdAt === 'object'
|
|
236
|
-
? task.createdAt.toNumber()
|
|
237
|
-
: task.createdAt,
|
|
227
|
+
createdAt: typeof task.createdAt === 'object' ? task.createdAt.toNumber() : task.createdAt,
|
|
238
228
|
},
|
|
239
229
|
dependencyStatus: {
|
|
240
230
|
isBlocked,
|
|
@@ -21,6 +21,7 @@ const SENSE_PHEROMONES_QUERY = `
|
|
|
21
21
|
AND ($agentIds IS NULL OR size($agentIds) = 0 OR p.agentId IN $agentIds)
|
|
22
22
|
AND ($swarmId IS NULL OR p.swarmId = $swarmId)
|
|
23
23
|
AND ($excludeAgentId IS NULL OR p.agentId <> $excludeAgentId)
|
|
24
|
+
AND ($sessionId IS NULL OR p.sessionId = $sessionId)
|
|
24
25
|
|
|
25
26
|
// Calculate current intensity with exponential decay
|
|
26
27
|
WITH p,
|
|
@@ -49,6 +50,7 @@ const SENSE_PHEROMONES_QUERY = `
|
|
|
49
50
|
p.timestamp AS timestamp,
|
|
50
51
|
p.data AS data,
|
|
51
52
|
p.halfLife AS halfLifeMs,
|
|
53
|
+
p.sessionId AS sessionId,
|
|
52
54
|
CASE WHEN target IS NOT NULL THEN labels(target)[0] ELSE null END AS targetType,
|
|
53
55
|
CASE WHEN target IS NOT NULL THEN target.name ELSE null END AS targetName,
|
|
54
56
|
CASE WHEN target IS NOT NULL THEN target.filePath ELSE null END AS targetFilePath
|
|
@@ -108,6 +110,10 @@ export const createSwarmSenseTool = (server) => {
|
|
|
108
110
|
.string()
|
|
109
111
|
.optional()
|
|
110
112
|
.describe('Exclude pheromones from this agent ID (useful for seeing what OTHER agents are doing)'),
|
|
113
|
+
sessionId: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe('Filter pheromones by session ID. Use to recover context after compaction or session restart.'),
|
|
111
117
|
minIntensity: z
|
|
112
118
|
.number()
|
|
113
119
|
.min(0)
|
|
@@ -130,7 +136,7 @@ export const createSwarmSenseTool = (server) => {
|
|
|
130
136
|
.default(false)
|
|
131
137
|
.describe('Run cleanup of fully decayed pheromones (intensity < 0.01)'),
|
|
132
138
|
},
|
|
133
|
-
}, async ({ projectId, types, nodeIds, agentIds, swarmId, excludeAgentId, minIntensity = 0.3, limit = 50, includeStats = false, cleanup = false, }) => {
|
|
139
|
+
}, async ({ projectId, types, nodeIds, agentIds, swarmId, excludeAgentId, sessionId, minIntensity = 0.3, limit = 50, includeStats = false, cleanup = false, }) => {
|
|
134
140
|
const neo4jService = new Neo4jService();
|
|
135
141
|
// Resolve project ID
|
|
136
142
|
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
@@ -162,6 +168,7 @@ export const createSwarmSenseTool = (server) => {
|
|
|
162
168
|
agentIds: agentIds ?? null,
|
|
163
169
|
swarmId: swarmId ?? null,
|
|
164
170
|
excludeAgentId: excludeAgentId ?? null,
|
|
171
|
+
sessionId: sessionId ?? null,
|
|
165
172
|
minIntensity,
|
|
166
173
|
limit: Math.floor(limit),
|
|
167
174
|
});
|
|
@@ -177,6 +184,7 @@ export const createSwarmSenseTool = (server) => {
|
|
|
177
184
|
originalIntensity: p.originalIntensity,
|
|
178
185
|
agentId: p.agentId,
|
|
179
186
|
swarmId: p.swarmId,
|
|
187
|
+
sessionId: p.sessionId ?? null,
|
|
180
188
|
timestamp: ts,
|
|
181
189
|
age: ts ? `${Math.round((Date.now() - ts) / 1000)}s ago` : null,
|
|
182
190
|
data: p.data ? JSON.parse(p.data) : null,
|
|
@@ -138,12 +138,24 @@ export const QUERIES = {
|
|
|
138
138
|
`,
|
|
139
139
|
CREATE_EMBEDDED_VECTOR_INDEX: `
|
|
140
140
|
CREATE VECTOR INDEX embedded_nodes_idx IF NOT EXISTS
|
|
141
|
-
FOR (n:Embedded) ON (n.embedding)
|
|
141
|
+
FOR (n:Embedded) ON (n.embedding)
|
|
142
142
|
OPTIONS {indexConfig: {
|
|
143
143
|
\`vector.dimensions\`: 3072,
|
|
144
144
|
\`vector.similarity_function\`: 'cosine'
|
|
145
145
|
}}
|
|
146
146
|
`,
|
|
147
|
+
CREATE_SESSION_NOTES_VECTOR_INDEX: `
|
|
148
|
+
CREATE VECTOR INDEX session_notes_idx IF NOT EXISTS
|
|
149
|
+
FOR (n:SessionNote) ON (n.embedding)
|
|
150
|
+
OPTIONS {indexConfig: {
|
|
151
|
+
\`vector.dimensions\`: 3072,
|
|
152
|
+
\`vector.similarity_function\`: 'cosine'
|
|
153
|
+
}}
|
|
154
|
+
`,
|
|
155
|
+
// Indexes for efficient SessionBookmark and SessionNote lookups
|
|
156
|
+
CREATE_SESSION_BOOKMARK_INDEX: 'CREATE INDEX session_bookmark_idx IF NOT EXISTS FOR (n:SessionBookmark) ON (n.projectId, n.sessionId)',
|
|
157
|
+
CREATE_SESSION_NOTE_INDEX: 'CREATE INDEX session_note_idx IF NOT EXISTS FOR (n:SessionNote) ON (n.projectId, n.sessionId)',
|
|
158
|
+
CREATE_SESSION_NOTE_CATEGORY_INDEX: 'CREATE INDEX session_note_category_idx IF NOT EXISTS FOR (n:SessionNote) ON (n.projectId, n.category)',
|
|
147
159
|
// Vector search with configurable fetch multiplier for project filtering.
|
|
148
160
|
// fetchMultiplier (default: 10) controls how many extra results to fetch before filtering by projectId.
|
|
149
161
|
// minSimilarity (default: 0.3) filters out low-confidence matches for nonsense queries.
|
package/package.json
CHANGED
|
@@ -1,469 +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.map(agentId => {
|
|
419
|
-
const prompt = workerPrompt.replace(/\{AGENT_ID\}/g, agentId);
|
|
420
|
-
return `Task({
|
|
421
|
-
subagent_type: "general-purpose",
|
|
422
|
-
run_in_background: false,
|
|
423
|
-
prompt: \`${prompt}\`
|
|
424
|
-
})`;
|
|
425
|
-
}).join('\n\n');
|
|
426
|
-
return `
|
|
427
|
-
## Worker Agent Instructions
|
|
428
|
-
|
|
429
|
-
Spawn ${recommendedAgents} worker agent(s) IN PARALLEL (all Task calls in ONE message):
|
|
430
|
-
|
|
431
|
-
\`\`\`javascript
|
|
432
|
-
${taskCalls}
|
|
433
|
-
\`\`\`
|
|
434
|
-
|
|
435
|
-
**CRITICAL:** Include ALL ${recommendedAgents} Task calls in a single message to run them in parallel.
|
|
436
|
-
|
|
437
|
-
## Monitoring Progress
|
|
438
|
-
|
|
439
|
-
Check swarm progress:
|
|
440
|
-
\`\`\`javascript
|
|
441
|
-
swarm_get_tasks({
|
|
442
|
-
projectId: "${projectId}",
|
|
443
|
-
swarmId: "${swarmId}",
|
|
444
|
-
includeStats: true
|
|
445
|
-
})
|
|
446
|
-
\`\`\`
|
|
447
|
-
|
|
448
|
-
## Cleanup (after all workers complete)
|
|
449
|
-
|
|
450
|
-
\`\`\`javascript
|
|
451
|
-
swarm_cleanup({
|
|
452
|
-
projectId: "${projectId}",
|
|
453
|
-
swarmId: "${swarmId}"
|
|
454
|
-
})
|
|
455
|
-
\`\`\`
|
|
456
|
-
`;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Build dependency graph edges for visualization
|
|
460
|
-
*/
|
|
461
|
-
function buildDependencyGraph(decomposition) {
|
|
462
|
-
const edges = [];
|
|
463
|
-
for (const task of decomposition.tasks) {
|
|
464
|
-
for (const depId of task.dependencies) {
|
|
465
|
-
edges.push({ from: depId, to: task.id });
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return edges;
|
|
469
|
-
}
|