code-graph-context 2.7.0 → 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/mcp/constants.js +16 -7
- package/dist/mcp/handlers/graph-generator.handler.js +3 -0
- package/dist/mcp/tools/index.js +3 -3
- package/dist/mcp/tools/session-bookmark.tool.js +31 -10
- package/dist/mcp/tools/session-cleanup.tool.js +139 -0
- package/dist/storage/neo4j/neo4j.service.js +4 -0
- package/package.json +1 -1
- package/dist/mcp/tools/swarm-orchestrate.tool.js +0 -471
package/README.md
CHANGED
|
@@ -345,7 +345,6 @@ Explicit task management with dependencies:
|
|
|
345
345
|
|
|
346
346
|
| Tool | Purpose |
|
|
347
347
|
|------|---------|
|
|
348
|
-
| `swarm_orchestrate` | Decompose a task and spawn worker agents |
|
|
349
348
|
| `swarm_post_task` | Add a task to the queue |
|
|
350
349
|
| `swarm_get_tasks` | Query tasks with filters |
|
|
351
350
|
| `swarm_claim_task` | Claim/start/release a task |
|
|
@@ -357,28 +356,20 @@ Explicit task management with dependencies:
|
|
|
357
356
|
### Example: Parallel Refactoring
|
|
358
357
|
|
|
359
358
|
```typescript
|
|
360
|
-
// Orchestrator decomposes and creates
|
|
361
|
-
|
|
359
|
+
// Orchestrator decomposes the task and creates individual work items
|
|
360
|
+
swarm_post_task({
|
|
362
361
|
projectId: "backend",
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
swarmId: "swarm_rename_user",
|
|
363
|
+
title: "Update UserService.findUserById",
|
|
364
|
+
description: "Rename getUserById to findUserById in UserService",
|
|
365
|
+
type: "refactor",
|
|
366
|
+
createdBy: "orchestrator"
|
|
365
367
|
})
|
|
366
368
|
|
|
367
|
-
//
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
totalTasks: 12,
|
|
372
|
-
parallelizable: 8,
|
|
373
|
-
sequential: 4, // These have dependencies
|
|
374
|
-
tasks: [
|
|
375
|
-
{ id: "task_1", title: "Update UserService.findUserById", status: "available" },
|
|
376
|
-
{ id: "task_2", title: "Update UserController references", status: "blocked", depends: ["task_1"] },
|
|
377
|
-
...
|
|
378
|
-
]
|
|
379
|
-
},
|
|
380
|
-
workerInstructions: "..." // Copy-paste to spawn workers
|
|
381
|
-
}
|
|
369
|
+
// Workers claim and execute tasks
|
|
370
|
+
swarm_claim_task({ projectId: "backend", swarmId: "swarm_rename_user", agentId: "worker_1" })
|
|
371
|
+
// ... do work ...
|
|
372
|
+
swarm_complete_task({ taskId: "task_1", agentId: "worker_1", action: "complete", summary: "Renamed method" })
|
|
382
373
|
```
|
|
383
374
|
|
|
384
375
|
### Install the Swarm Skill
|
|
@@ -428,7 +419,6 @@ See [`skills/swarm/SKILL.md`](skills/swarm/SKILL.md) for the full documentation.
|
|
|
428
419
|
| `stop_watch_project` | Stop file watching |
|
|
429
420
|
| `list_watchers` | List active file watchers |
|
|
430
421
|
| **Swarm** | |
|
|
431
|
-
| `swarm_orchestrate` | Plan and spawn parallel agents |
|
|
432
422
|
| `swarm_post_task` | Add task to the queue |
|
|
433
423
|
| `swarm_get_tasks` | Query tasks |
|
|
434
424
|
| `swarm_claim_task` | Claim/start/release tasks |
|
|
@@ -458,7 +448,7 @@ detect_dead_code → detect_duplicate_code → prioritize cleanup
|
|
|
458
448
|
|
|
459
449
|
**Pattern 4: Multi-Agent Work**
|
|
460
450
|
```
|
|
461
|
-
|
|
451
|
+
swarm_post_task → swarm_claim_task → swarm_complete_task → swarm_get_tasks(includeStats) → swarm_cleanup
|
|
462
452
|
```
|
|
463
453
|
|
|
464
454
|
### Multi-Project Support
|
package/dist/mcp/constants.js
CHANGED
|
@@ -38,11 +38,11 @@ export const TOOL_NAMES = {
|
|
|
38
38
|
swarmClaimTask: 'swarm_claim_task',
|
|
39
39
|
swarmCompleteTask: 'swarm_complete_task',
|
|
40
40
|
swarmGetTasks: 'swarm_get_tasks',
|
|
41
|
-
swarmOrchestrate: 'swarm_orchestrate',
|
|
42
41
|
saveSessionBookmark: 'save_session_bookmark',
|
|
43
42
|
restoreSessionBookmark: 'restore_session_bookmark',
|
|
44
43
|
saveSessionNote: 'save_session_note',
|
|
45
44
|
recallSessionNotes: 'recall_session_notes',
|
|
45
|
+
cleanupSession: 'cleanup_session',
|
|
46
46
|
};
|
|
47
47
|
// Tool Metadata
|
|
48
48
|
export const TOOL_METADATA = {
|
|
@@ -258,12 +258,6 @@ Complete unblocks dependent tasks. Failed tasks can be retried if retryable=true
|
|
|
258
258
|
description: `Query tasks with filters. Use taskId for single task, or filter by swarmId, statuses, types, claimedBy, createdBy, minPriority.
|
|
259
259
|
|
|
260
260
|
Sort by: priority (default), created, updated. Add includeStats:true for aggregate counts.`,
|
|
261
|
-
},
|
|
262
|
-
[TOOL_NAMES.swarmOrchestrate]: {
|
|
263
|
-
title: 'Swarm Orchestrate',
|
|
264
|
-
description: `Coordinate multiple agents for complex multi-file tasks. Analyzes codebase, decomposes into atomic tasks, spawns workers, monitors progress.
|
|
265
|
-
|
|
266
|
-
Use dryRun:true to preview plan. maxAgents controls parallelism (default: 3). Failed tasks auto-retry via pheromone decay.`,
|
|
267
261
|
},
|
|
268
262
|
[TOOL_NAMES.saveSessionBookmark]: {
|
|
269
263
|
title: 'Save Session Bookmark',
|
|
@@ -334,6 +328,21 @@ Parameters:
|
|
|
334
328
|
|
|
335
329
|
Returns notes with topic, content, category, severity, relevance score (vector mode), and linked aboutNodes.`,
|
|
336
330
|
},
|
|
331
|
+
[TOOL_NAMES.cleanupSession]: {
|
|
332
|
+
title: 'Cleanup Session',
|
|
333
|
+
description: `Clean up expired session notes and old session bookmarks.
|
|
334
|
+
|
|
335
|
+
Removes:
|
|
336
|
+
- Expired SessionNote nodes (past expiresAt) and their edges
|
|
337
|
+
- Old SessionBookmark nodes, keeping only the most recent N per session (default: 3)
|
|
338
|
+
|
|
339
|
+
Parameters:
|
|
340
|
+
- projectId (required): Project ID, name, or path
|
|
341
|
+
- keepBookmarks (default: 3): Number of most recent bookmarks to keep per session
|
|
342
|
+
- dryRun (default: false): Preview what would be deleted without deleting
|
|
343
|
+
|
|
344
|
+
Returns counts of deleted notes, bookmarks, and edges.`,
|
|
345
|
+
},
|
|
337
346
|
};
|
|
338
347
|
// Default Values
|
|
339
348
|
export const DEFAULTS = {
|
|
@@ -74,6 +74,9 @@ export class GraphGeneratorHandler {
|
|
|
74
74
|
await this.neo4jService.run(QUERIES.CREATE_PROJECT_ID_INDEX_EMBEDDED);
|
|
75
75
|
await this.neo4jService.run(QUERIES.CREATE_PROJECT_ID_INDEX_SOURCEFILE);
|
|
76
76
|
await this.neo4jService.run(QUERIES.CREATE_NORMALIZED_HASH_INDEX);
|
|
77
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_BOOKMARK_INDEX);
|
|
78
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_NOTE_INDEX);
|
|
79
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_NOTE_CATEGORY_INDEX);
|
|
77
80
|
await debugLog('Project indexes created');
|
|
78
81
|
}
|
|
79
82
|
async importNodes(nodes, batchSize) {
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.
|
|
|
13
13
|
import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
|
|
14
14
|
import { createSearchCodebaseTool } from './search-codebase.tool.js';
|
|
15
15
|
import { createRestoreSessionBookmarkTool, createSaveSessionBookmarkTool } from './session-bookmark.tool.js';
|
|
16
|
+
import { createCleanupSessionTool } from './session-cleanup.tool.js';
|
|
16
17
|
import { createRecallSessionNotesTool, createSaveSessionNoteTool } from './session-note.tool.js';
|
|
17
18
|
import { createStartWatchProjectTool } from './start-watch-project.tool.js';
|
|
18
19
|
import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
|
|
@@ -20,7 +21,6 @@ import { createSwarmClaimTaskTool } from './swarm-claim-task.tool.js';
|
|
|
20
21
|
import { createSwarmCleanupTool } from './swarm-cleanup.tool.js';
|
|
21
22
|
import { createSwarmCompleteTaskTool } from './swarm-complete-task.tool.js';
|
|
22
23
|
import { createSwarmGetTasksTool } from './swarm-get-tasks.tool.js';
|
|
23
|
-
import { createSwarmOrchestrateTool } from './swarm-orchestrate.tool.js';
|
|
24
24
|
import { createSwarmPheromoneTool } from './swarm-pheromone.tool.js';
|
|
25
25
|
import { createSwarmPostTaskTool } from './swarm-post-task.tool.js';
|
|
26
26
|
import { createSwarmSenseTool } from './swarm-sense.tool.js';
|
|
@@ -73,12 +73,12 @@ export const registerAllTools = (server) => {
|
|
|
73
73
|
createSwarmClaimTaskTool(server);
|
|
74
74
|
createSwarmCompleteTaskTool(server);
|
|
75
75
|
createSwarmGetTasksTool(server);
|
|
76
|
-
// Register swarm orchestration tool (meta-tool for coordinating multi-agent work)
|
|
77
|
-
createSwarmOrchestrateTool(server);
|
|
78
76
|
// Register session bookmark tools (cross-session context continuity)
|
|
79
77
|
createSaveSessionBookmarkTool(server);
|
|
80
78
|
createRestoreSessionBookmarkTool(server);
|
|
81
79
|
// Register session note tools (durable observations and decisions)
|
|
82
80
|
createSaveSessionNoteTool(server);
|
|
83
81
|
createRecallSessionNotesTool(server);
|
|
82
|
+
// Register session cleanup tool
|
|
83
|
+
createCleanupSessionTool(server);
|
|
84
84
|
};
|
|
@@ -96,11 +96,24 @@ const GET_SESSION_NOTES_QUERY = `
|
|
|
96
96
|
MATCH (n:SessionNote)
|
|
97
97
|
WHERE n.projectId = $projectId
|
|
98
98
|
AND n.sessionId = $sessionId
|
|
99
|
+
AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
|
|
100
|
+
|
|
101
|
+
OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
|
|
102
|
+
WHERE NOT codeNode:SessionNote
|
|
103
|
+
AND NOT codeNode:SessionBookmark
|
|
104
|
+
AND NOT codeNode:Pheromone
|
|
105
|
+
AND NOT codeNode:SwarmTask
|
|
106
|
+
|
|
99
107
|
RETURN n.id AS id,
|
|
100
|
-
n.
|
|
101
|
-
n.agentId AS agentId,
|
|
108
|
+
n.topic AS topic,
|
|
102
109
|
n.content AS content,
|
|
103
|
-
n.
|
|
110
|
+
n.category AS category,
|
|
111
|
+
n.severity AS severity,
|
|
112
|
+
n.agentId AS agentId,
|
|
113
|
+
n.sessionId AS sessionId,
|
|
114
|
+
n.createdAt AS createdAt,
|
|
115
|
+
n.expiresAt AS expiresAt,
|
|
116
|
+
collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
|
|
104
117
|
ORDER BY n.createdAt ASC
|
|
105
118
|
LIMIT 50
|
|
106
119
|
`;
|
|
@@ -266,13 +279,21 @@ export const createRestoreSessionBookmarkTool = (server) => {
|
|
|
266
279
|
}
|
|
267
280
|
return node;
|
|
268
281
|
});
|
|
269
|
-
const notes = noteRows.map((n) =>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
const notes = noteRows.map((n) => {
|
|
283
|
+
const aboutNodes = (n.aboutNodes ?? []).filter((node) => node?.id != null);
|
|
284
|
+
return {
|
|
285
|
+
id: n.id,
|
|
286
|
+
topic: n.topic,
|
|
287
|
+
content: n.content,
|
|
288
|
+
category: n.category,
|
|
289
|
+
severity: n.severity,
|
|
290
|
+
agentId: n.agentId,
|
|
291
|
+
sessionId: n.sessionId,
|
|
292
|
+
createdAt: typeof n.createdAt === 'object' && n.createdAt?.toNumber ? n.createdAt.toNumber() : n.createdAt,
|
|
293
|
+
expiresAt: typeof n.expiresAt === 'object' && n.expiresAt?.toNumber ? n.expiresAt.toNumber() : n.expiresAt,
|
|
294
|
+
aboutNodes,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
276
297
|
// Identify working set nodes not found in the graph (stale IDs after re-parse)
|
|
277
298
|
const foundIds = new Set(workingSetRows.map((r) => r.id));
|
|
278
299
|
const storedIds = Array.isArray(bm.workingSetNodeIds) ? bm.workingSetNodeIds : [];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Cleanup Tool
|
|
3
|
+
* Remove expired notes and prune old bookmarks
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Count expired notes (for dry run)
|
|
11
|
+
*/
|
|
12
|
+
const COUNT_EXPIRED_NOTES_QUERY = `
|
|
13
|
+
MATCH (n:SessionNote)
|
|
14
|
+
WHERE n.projectId = $projectId
|
|
15
|
+
AND n.expiresAt IS NOT NULL
|
|
16
|
+
AND n.expiresAt <= timestamp()
|
|
17
|
+
RETURN count(n) AS count
|
|
18
|
+
`;
|
|
19
|
+
/**
|
|
20
|
+
* Delete expired notes and their edges
|
|
21
|
+
*/
|
|
22
|
+
const DELETE_EXPIRED_NOTES_QUERY = `
|
|
23
|
+
MATCH (n:SessionNote)
|
|
24
|
+
WHERE n.projectId = $projectId
|
|
25
|
+
AND n.expiresAt IS NOT NULL
|
|
26
|
+
AND n.expiresAt <= timestamp()
|
|
27
|
+
WITH collect(n) AS toDelete
|
|
28
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
29
|
+
FOREACH (n IN toDelete | DETACH DELETE n)
|
|
30
|
+
RETURN cnt AS deleted
|
|
31
|
+
`;
|
|
32
|
+
/**
|
|
33
|
+
* Find old bookmarks to prune (keeping N most recent per session)
|
|
34
|
+
*/
|
|
35
|
+
const COUNT_OLD_BOOKMARKS_QUERY = `
|
|
36
|
+
MATCH (b:SessionBookmark)
|
|
37
|
+
WHERE b.projectId = $projectId
|
|
38
|
+
WITH b.sessionId AS sessionId, b
|
|
39
|
+
ORDER BY b.createdAt DESC
|
|
40
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
41
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
42
|
+
UNWIND bookmarks[$keepBookmarks..] AS old
|
|
43
|
+
RETURN count(old) AS count
|
|
44
|
+
`;
|
|
45
|
+
/**
|
|
46
|
+
* Delete old bookmarks (keeping N most recent per session)
|
|
47
|
+
*/
|
|
48
|
+
const DELETE_OLD_BOOKMARKS_QUERY = `
|
|
49
|
+
MATCH (b:SessionBookmark)
|
|
50
|
+
WHERE b.projectId = $projectId
|
|
51
|
+
WITH b.sessionId AS sessionId, b
|
|
52
|
+
ORDER BY b.createdAt DESC
|
|
53
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
54
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
55
|
+
WITH reduce(all = [], bs IN collect(bookmarks[$keepBookmarks..]) | all + bs) AS toDelete
|
|
56
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
57
|
+
FOREACH (b IN toDelete | DETACH DELETE b)
|
|
58
|
+
RETURN cnt AS deleted
|
|
59
|
+
`;
|
|
60
|
+
export const createCleanupSessionTool = (server) => {
|
|
61
|
+
server.registerTool(TOOL_NAMES.cleanupSession, {
|
|
62
|
+
title: TOOL_METADATA[TOOL_NAMES.cleanupSession].title,
|
|
63
|
+
description: TOOL_METADATA[TOOL_NAMES.cleanupSession].description,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
66
|
+
keepBookmarks: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(50)
|
|
71
|
+
.optional()
|
|
72
|
+
.default(3)
|
|
73
|
+
.describe('Number of most recent bookmarks to keep per session (default: 3)'),
|
|
74
|
+
dryRun: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.optional()
|
|
77
|
+
.default(false)
|
|
78
|
+
.describe('Preview what would be deleted without deleting (default: false)'),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ projectId, keepBookmarks = 3, dryRun = false }) => {
|
|
81
|
+
const neo4jService = new Neo4jService();
|
|
82
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
83
|
+
if (!projectResult.success) {
|
|
84
|
+
await neo4jService.close();
|
|
85
|
+
return projectResult.error;
|
|
86
|
+
}
|
|
87
|
+
const resolvedProjectId = projectResult.projectId;
|
|
88
|
+
try {
|
|
89
|
+
const params = { projectId: resolvedProjectId, keepBookmarks };
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
const [noteCount, bookmarkCount] = await Promise.all([
|
|
92
|
+
neo4jService.run(COUNT_EXPIRED_NOTES_QUERY, params),
|
|
93
|
+
neo4jService.run(COUNT_OLD_BOOKMARKS_QUERY, params),
|
|
94
|
+
]);
|
|
95
|
+
const expiredNotes = noteCount[0]?.count ?? 0;
|
|
96
|
+
const oldBookmarks = bookmarkCount[0]?.count ?? 0;
|
|
97
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
98
|
+
return createSuccessResponse(JSON.stringify({
|
|
99
|
+
dryRun: true,
|
|
100
|
+
projectId: resolvedProjectId,
|
|
101
|
+
wouldDelete: {
|
|
102
|
+
expiredNotes: toNumber(expiredNotes),
|
|
103
|
+
oldBookmarks: toNumber(oldBookmarks),
|
|
104
|
+
},
|
|
105
|
+
keepBookmarks,
|
|
106
|
+
message: toNumber(expiredNotes) === 0 && toNumber(oldBookmarks) === 0
|
|
107
|
+
? 'Nothing to clean up.'
|
|
108
|
+
: `Would delete ${toNumber(expiredNotes)} expired notes and ${toNumber(oldBookmarks)} old bookmarks.`,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const [noteResult, bookmarkResult] = await Promise.all([
|
|
112
|
+
neo4jService.run(DELETE_EXPIRED_NOTES_QUERY, params),
|
|
113
|
+
neo4jService.run(DELETE_OLD_BOOKMARKS_QUERY, params),
|
|
114
|
+
]);
|
|
115
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
116
|
+
const deletedNotes = toNumber(noteResult[0]?.deleted ?? 0);
|
|
117
|
+
const deletedBookmarks = toNumber(bookmarkResult[0]?.deleted ?? 0);
|
|
118
|
+
return createSuccessResponse(JSON.stringify({
|
|
119
|
+
success: true,
|
|
120
|
+
projectId: resolvedProjectId,
|
|
121
|
+
deleted: {
|
|
122
|
+
expiredNotes: deletedNotes,
|
|
123
|
+
oldBookmarks: deletedBookmarks,
|
|
124
|
+
},
|
|
125
|
+
keepBookmarks,
|
|
126
|
+
message: deletedNotes === 0 && deletedBookmarks === 0
|
|
127
|
+
? 'Nothing to clean up.'
|
|
128
|
+
: `Deleted ${deletedNotes} expired notes and ${deletedBookmarks} old bookmarks.`,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await debugLog('Cleanup session error', { error: String(error) });
|
|
133
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await neo4jService.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
};
|
|
@@ -152,6 +152,10 @@ export const QUERIES = {
|
|
|
152
152
|
\`vector.similarity_function\`: 'cosine'
|
|
153
153
|
}}
|
|
154
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)',
|
|
155
159
|
// Vector search with configurable fetch multiplier for project filtering.
|
|
156
160
|
// fetchMultiplier (default: 10) controls how many extra results to fetch before filtering by projectId.
|
|
157
161
|
// minSimilarity (default: 0.3) filters out low-confidence matches for nonsense queries.
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|