claudehq 1.0.2 → 1.0.3

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.
@@ -0,0 +1,941 @@
1
+ /**
2
+ * Orchestration Data Module - Multi-Agent Orchestration Management
3
+ *
4
+ * Handles creating, updating, and managing orchestrations and their agents.
5
+ * Orchestrations coordinate multiple Claude agents working in parallel.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+
12
+ const { ORCHESTRATIONS_DIR, AGENT_STATUS, ORCHESTRATION_STATUS } = require('../core/config');
13
+ const { eventBus, EventTypes } = require('../core/event-bus');
14
+
15
+ /**
16
+ * Built-in orchestration templates
17
+ */
18
+ const ORCHESTRATION_TEMPLATES = {
19
+ 'code-review': {
20
+ id: 'code-review',
21
+ name: 'Code Review Pipeline',
22
+ description: 'Analyze code, review for issues, then fix problems',
23
+ icon: '🔍',
24
+ agents: [
25
+ {
26
+ name: 'Code Analyzer',
27
+ model: 'sonnet',
28
+ prompt: 'Analyze the codebase structure and identify the main components, patterns, and potential areas of concern. Create a summary of findings.',
29
+ dependencies: []
30
+ },
31
+ {
32
+ name: 'Code Reviewer',
33
+ model: 'sonnet',
34
+ prompt: 'Review the code for bugs, security issues, performance problems, and code quality. List all issues found with severity ratings.',
35
+ dependencies: [] // Will be set to depend on Analyzer
36
+ },
37
+ {
38
+ name: 'Code Fixer',
39
+ model: 'sonnet',
40
+ prompt: 'Based on the review findings, fix the identified issues. Make the necessary code changes and explain what was fixed.',
41
+ dependencies: [] // Will be set to depend on Reviewer
42
+ }
43
+ ]
44
+ },
45
+ 'full-stack-feature': {
46
+ id: 'full-stack-feature',
47
+ name: 'Full-Stack Feature',
48
+ description: 'Build frontend, backend, and tests in parallel, then integrate',
49
+ icon: '🏗️',
50
+ agents: [
51
+ {
52
+ name: 'Frontend Developer',
53
+ model: 'sonnet',
54
+ prompt: 'Implement the frontend components for this feature. Create the UI, handle user interactions, and manage state.',
55
+ dependencies: []
56
+ },
57
+ {
58
+ name: 'Backend Developer',
59
+ model: 'sonnet',
60
+ prompt: 'Implement the backend API endpoints for this feature. Create the routes, controllers, and data models.',
61
+ dependencies: []
62
+ },
63
+ {
64
+ name: 'Test Engineer',
65
+ model: 'haiku',
66
+ prompt: 'Write comprehensive tests for this feature including unit tests, integration tests, and edge cases.',
67
+ dependencies: []
68
+ },
69
+ {
70
+ name: 'Integration Lead',
71
+ model: 'sonnet',
72
+ prompt: 'Integrate the frontend and backend components. Ensure they work together correctly and fix any integration issues.',
73
+ dependencies: [] // Will depend on Frontend and Backend
74
+ }
75
+ ]
76
+ },
77
+ 'documentation': {
78
+ id: 'documentation',
79
+ name: 'Documentation Generator',
80
+ description: 'Read code, write documentation, then review for quality',
81
+ icon: '📚',
82
+ agents: [
83
+ {
84
+ name: 'Code Reader',
85
+ model: 'haiku',
86
+ prompt: 'Read and understand the codebase. Identify all public APIs, functions, classes, and their purposes.',
87
+ dependencies: []
88
+ },
89
+ {
90
+ name: 'Doc Writer',
91
+ model: 'sonnet',
92
+ prompt: 'Write comprehensive documentation including API reference, usage examples, and architecture overview.',
93
+ dependencies: [] // Will depend on Code Reader
94
+ },
95
+ {
96
+ name: 'Doc Reviewer',
97
+ model: 'haiku',
98
+ prompt: 'Review the documentation for accuracy, clarity, and completeness. Suggest improvements.',
99
+ dependencies: [] // Will depend on Doc Writer
100
+ }
101
+ ]
102
+ },
103
+ 'bug-fix': {
104
+ id: 'bug-fix',
105
+ name: 'Bug Investigation & Fix',
106
+ description: 'Investigate bug, propose fix, implement and test',
107
+ icon: '🐛',
108
+ agents: [
109
+ {
110
+ name: 'Bug Investigator',
111
+ model: 'sonnet',
112
+ prompt: 'Investigate the reported bug. Find the root cause by examining logs, code, and reproducing the issue.',
113
+ dependencies: []
114
+ },
115
+ {
116
+ name: 'Fix Implementer',
117
+ model: 'sonnet',
118
+ prompt: 'Implement a fix for the bug based on the investigation findings. Make the necessary code changes.',
119
+ dependencies: [] // Will depend on Investigator
120
+ },
121
+ {
122
+ name: 'Test Verifier',
123
+ model: 'haiku',
124
+ prompt: 'Write tests to verify the bug is fixed and add regression tests to prevent future occurrences.',
125
+ dependencies: [] // Will depend on Implementer
126
+ }
127
+ ]
128
+ },
129
+ 'refactor': {
130
+ id: 'refactor',
131
+ name: 'Code Refactoring',
132
+ description: 'Analyze, plan, and execute a code refactoring',
133
+ icon: '♻️',
134
+ agents: [
135
+ {
136
+ name: 'Architecture Analyst',
137
+ model: 'sonnet',
138
+ prompt: 'Analyze the current code architecture and identify areas that need refactoring. Document technical debt.',
139
+ dependencies: []
140
+ },
141
+ {
142
+ name: 'Refactor Planner',
143
+ model: 'sonnet',
144
+ prompt: 'Create a detailed refactoring plan with step-by-step changes that maintain backward compatibility.',
145
+ dependencies: [] // Will depend on Analyst
146
+ },
147
+ {
148
+ name: 'Refactor Executor',
149
+ model: 'sonnet',
150
+ prompt: 'Execute the refactoring plan. Make incremental changes and ensure tests pass after each change.',
151
+ dependencies: [] // Will depend on Planner
152
+ }
153
+ ]
154
+ }
155
+ };
156
+
157
+ /**
158
+ * Get all available templates
159
+ * @returns {Array} Array of template summaries
160
+ */
161
+ function getTemplates() {
162
+ return Object.values(ORCHESTRATION_TEMPLATES).map(t => ({
163
+ id: t.id,
164
+ name: t.name,
165
+ description: t.description,
166
+ icon: t.icon,
167
+ agentCount: t.agents.length
168
+ }));
169
+ }
170
+
171
+ /**
172
+ * Get a specific template by ID
173
+ * @param {string} templateId - Template ID
174
+ * @returns {Object|null} Template or null
175
+ */
176
+ function getTemplate(templateId) {
177
+ return ORCHESTRATION_TEMPLATES[templateId] || null;
178
+ }
179
+
180
+ /**
181
+ * Create orchestration from template
182
+ * @param {string} templateId - Template ID
183
+ * @param {Object} options - Override options
184
+ * @returns {Object} Result with orchestration or error
185
+ */
186
+ function createFromTemplate(templateId, options = {}) {
187
+ const template = getTemplate(templateId);
188
+
189
+ if (!template) {
190
+ return { error: 'Template not found' };
191
+ }
192
+
193
+ // Create orchestration config from template
194
+ const config = {
195
+ name: options.name || template.name,
196
+ description: options.description || template.description,
197
+ templateId: templateId,
198
+ agents: []
199
+ };
200
+
201
+ // Create agents and set up dependencies
202
+ const agentIdMap = new Map(); // Map from index to generated ID
203
+
204
+ // First pass: create agents
205
+ template.agents.forEach((agentConfig, index) => {
206
+ const agent = {
207
+ name: agentConfig.name,
208
+ model: agentConfig.model,
209
+ prompt: options.customPrompts?.[index] || agentConfig.prompt,
210
+ workingDirectory: options.workingDirectory || process.cwd(),
211
+ dependencies: []
212
+ };
213
+ config.agents.push(agent);
214
+ });
215
+
216
+ // Second pass: set up dependencies based on template structure
217
+ // For sequential templates, each agent depends on the previous
218
+ if (['code-review', 'documentation', 'bug-fix', 'refactor'].includes(templateId)) {
219
+ for (let i = 1; i < config.agents.length; i++) {
220
+ // Dependency will be resolved after agents are created
221
+ config.agents[i]._dependsOnIndex = i - 1;
222
+ }
223
+ }
224
+
225
+ // For full-stack-feature, the Integration Lead depends on Frontend and Backend
226
+ if (templateId === 'full-stack-feature') {
227
+ config.agents[3]._dependsOnIndex = [0, 1]; // Integration depends on Frontend and Backend
228
+ }
229
+
230
+ // Create the orchestration
231
+ const result = createOrchestration(config);
232
+
233
+ if (result.error) {
234
+ return result;
235
+ }
236
+
237
+ // Now set up dependencies using actual agent IDs
238
+ const orch = result.orchestration;
239
+ const updates = [];
240
+
241
+ orch.agents.forEach((agent, index) => {
242
+ const templateAgent = config.agents[index];
243
+ if (templateAgent._dependsOnIndex !== undefined) {
244
+ const depIndexes = Array.isArray(templateAgent._dependsOnIndex)
245
+ ? templateAgent._dependsOnIndex
246
+ : [templateAgent._dependsOnIndex];
247
+
248
+ depIndexes.forEach(depIndex => {
249
+ const depAgentId = orch.agents[depIndex]?.id;
250
+ if (depAgentId) {
251
+ addAgentDependency(orch.id, agent.id, depAgentId);
252
+ }
253
+ });
254
+ }
255
+ });
256
+
257
+ // Reload the orchestration to get updated dependencies
258
+ return { success: true, orchestration: getOrchestration(orch.id) };
259
+ }
260
+
261
+ /**
262
+ * Generate a unique ID for orchestrations and agents
263
+ * @param {string} prefix - Prefix for the ID
264
+ * @returns {string} Unique ID
265
+ */
266
+ function generateId(prefix = 'orch') {
267
+ return `${prefix}-${crypto.randomBytes(4).toString('hex')}`;
268
+ }
269
+
270
+ /**
271
+ * Ensure orchestrations directory exists
272
+ */
273
+ function ensureDir() {
274
+ if (!fs.existsSync(ORCHESTRATIONS_DIR)) {
275
+ fs.mkdirSync(ORCHESTRATIONS_DIR, { recursive: true });
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Get the file path for an orchestration
281
+ * @param {string} orchestrationId - Orchestration ID
282
+ * @returns {string} File path
283
+ */
284
+ function getOrchestrationPath(orchestrationId) {
285
+ return path.join(ORCHESTRATIONS_DIR, `${orchestrationId}.json`);
286
+ }
287
+
288
+ /**
289
+ * Create a new agent object
290
+ * @param {Object} config - Agent configuration
291
+ * @returns {Object} Agent object
292
+ */
293
+ function createAgentObject(config) {
294
+ return {
295
+ id: generateId('agent'),
296
+ name: config.name || 'Unnamed Agent',
297
+ type: config.type || 'general-purpose', // general-purpose, explore, plan, custom
298
+ status: AGENT_STATUS.PENDING,
299
+ prompt: config.prompt || '',
300
+ model: config.model || 'sonnet', // haiku, sonnet, opus
301
+ workingDirectory: config.workingDirectory || process.cwd(),
302
+ sessionId: null, // Will be set when agent is spawned
303
+ tmuxWindow: null, // tmux window name
304
+ parentAgentId: config.parentAgentId || null,
305
+ dependencies: config.dependencies || [], // Agent IDs this agent depends on
306
+ createdAt: new Date().toISOString(),
307
+ startedAt: null,
308
+ completedAt: null,
309
+ error: null,
310
+ output: null,
311
+ toolsUsed: [],
312
+ metadata: config.metadata || {}
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Create a new orchestration
318
+ * @param {Object} config - Orchestration configuration
319
+ * @returns {Object} Result with orchestration or error
320
+ */
321
+ function createOrchestration(config) {
322
+ ensureDir();
323
+
324
+ const orchestration = {
325
+ id: generateId('orch'),
326
+ name: config.name || 'Unnamed Orchestration',
327
+ description: config.description || '',
328
+ status: ORCHESTRATION_STATUS.DRAFT,
329
+ agents: [],
330
+ createdAt: new Date().toISOString(),
331
+ startedAt: null,
332
+ completedAt: null,
333
+ templateId: config.templateId || null,
334
+ metadata: config.metadata || {}
335
+ };
336
+
337
+ // Add initial agents if provided
338
+ if (config.agents && Array.isArray(config.agents)) {
339
+ for (const agentConfig of config.agents) {
340
+ orchestration.agents.push(createAgentObject(agentConfig));
341
+ }
342
+ }
343
+
344
+ try {
345
+ const filePath = getOrchestrationPath(orchestration.id);
346
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
347
+
348
+ eventBus.emit(EventTypes.ORCHESTRATION_CREATED, { orchestration });
349
+
350
+ return { success: true, orchestration };
351
+ } catch (e) {
352
+ return { error: e.message };
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Get an orchestration by ID
358
+ * @param {string} orchestrationId - Orchestration ID
359
+ * @returns {Object|null} Orchestration object or null
360
+ */
361
+ function getOrchestration(orchestrationId) {
362
+ const filePath = getOrchestrationPath(orchestrationId);
363
+
364
+ if (!fs.existsSync(filePath)) {
365
+ return null;
366
+ }
367
+
368
+ try {
369
+ const content = fs.readFileSync(filePath, 'utf-8');
370
+ return JSON.parse(content);
371
+ } catch (e) {
372
+ console.error(`Error reading orchestration ${orchestrationId}:`, e.message);
373
+ return null;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * List all orchestrations
379
+ * @returns {Array} Array of orchestration objects
380
+ */
381
+ function listOrchestrations() {
382
+ ensureDir();
383
+
384
+ const orchestrations = [];
385
+
386
+ try {
387
+ const files = fs.readdirSync(ORCHESTRATIONS_DIR).filter(f => f.endsWith('.json'));
388
+
389
+ for (const file of files) {
390
+ try {
391
+ const content = fs.readFileSync(path.join(ORCHESTRATIONS_DIR, file), 'utf-8');
392
+ const orchestration = JSON.parse(content);
393
+ orchestrations.push(orchestration);
394
+ } catch (e) {
395
+ console.error(`Error reading ${file}:`, e.message);
396
+ }
397
+ }
398
+
399
+ // Sort by creation date, newest first
400
+ orchestrations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
401
+ } catch (e) {
402
+ console.error('Error listing orchestrations:', e.message);
403
+ }
404
+
405
+ return orchestrations;
406
+ }
407
+
408
+ /**
409
+ * Update an orchestration
410
+ * @param {string} orchestrationId - Orchestration ID
411
+ * @param {Object} updates - Updates to apply
412
+ * @returns {Object} Result with updated orchestration or error
413
+ */
414
+ function updateOrchestration(orchestrationId, updates) {
415
+ const orchestration = getOrchestration(orchestrationId);
416
+
417
+ if (!orchestration) {
418
+ return { error: 'Orchestration not found' };
419
+ }
420
+
421
+ const previousStatus = orchestration.status;
422
+
423
+ // Apply updates
424
+ if (updates.name !== undefined) orchestration.name = updates.name;
425
+ if (updates.description !== undefined) orchestration.description = updates.description;
426
+ if (updates.status !== undefined) orchestration.status = updates.status;
427
+ if (updates.startedAt !== undefined) orchestration.startedAt = updates.startedAt;
428
+ if (updates.completedAt !== undefined) orchestration.completedAt = updates.completedAt;
429
+ if (updates.metadata !== undefined) {
430
+ orchestration.metadata = { ...orchestration.metadata, ...updates.metadata };
431
+ }
432
+
433
+ try {
434
+ const filePath = getOrchestrationPath(orchestrationId);
435
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
436
+
437
+ eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
438
+ orchestrationId,
439
+ orchestration,
440
+ updates,
441
+ previousStatus
442
+ });
443
+
444
+ // Emit specific status events
445
+ if (updates.status && updates.status !== previousStatus) {
446
+ switch (updates.status) {
447
+ case ORCHESTRATION_STATUS.RUNNING:
448
+ eventBus.emit(EventTypes.ORCHESTRATION_STARTED, { orchestrationId, orchestration });
449
+ break;
450
+ case ORCHESTRATION_STATUS.COMPLETED:
451
+ eventBus.emit(EventTypes.ORCHESTRATION_COMPLETED, { orchestrationId, orchestration });
452
+ break;
453
+ case ORCHESTRATION_STATUS.FAILED:
454
+ eventBus.emit(EventTypes.ORCHESTRATION_FAILED, { orchestrationId, orchestration });
455
+ break;
456
+ case ORCHESTRATION_STATUS.PAUSED:
457
+ eventBus.emit(EventTypes.ORCHESTRATION_PAUSED, { orchestrationId, orchestration });
458
+ break;
459
+ }
460
+ }
461
+
462
+ return { success: true, orchestration };
463
+ } catch (e) {
464
+ return { error: e.message };
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Delete an orchestration
470
+ * @param {string} orchestrationId - Orchestration ID
471
+ * @returns {Object} Result with success flag or error
472
+ */
473
+ function deleteOrchestration(orchestrationId) {
474
+ const filePath = getOrchestrationPath(orchestrationId);
475
+
476
+ if (!fs.existsSync(filePath)) {
477
+ return { error: 'Orchestration not found' };
478
+ }
479
+
480
+ try {
481
+ const orchestration = getOrchestration(orchestrationId);
482
+ fs.unlinkSync(filePath);
483
+
484
+ eventBus.emit(EventTypes.ORCHESTRATION_DELETED, { orchestrationId, orchestration });
485
+
486
+ return { success: true };
487
+ } catch (e) {
488
+ return { error: e.message };
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Add an agent to an orchestration
494
+ * @param {string} orchestrationId - Orchestration ID
495
+ * @param {Object} agentConfig - Agent configuration
496
+ * @returns {Object} Result with agent or error
497
+ */
498
+ function addAgent(orchestrationId, agentConfig) {
499
+ const orchestration = getOrchestration(orchestrationId);
500
+
501
+ if (!orchestration) {
502
+ return { error: 'Orchestration not found' };
503
+ }
504
+
505
+ if (orchestration.status === ORCHESTRATION_STATUS.COMPLETED) {
506
+ return { error: 'Cannot add agents to a completed orchestration' };
507
+ }
508
+
509
+ const agent = createAgentObject(agentConfig);
510
+ orchestration.agents.push(agent);
511
+
512
+ try {
513
+ const filePath = getOrchestrationPath(orchestrationId);
514
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
515
+
516
+ eventBus.emit(EventTypes.AGENT_CREATED, { orchestrationId, agent });
517
+
518
+ return { success: true, agent };
519
+ } catch (e) {
520
+ return { error: e.message };
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Get an agent from an orchestration
526
+ * @param {string} orchestrationId - Orchestration ID
527
+ * @param {string} agentId - Agent ID
528
+ * @returns {Object|null} Agent object or null
529
+ */
530
+ function getAgent(orchestrationId, agentId) {
531
+ const orchestration = getOrchestration(orchestrationId);
532
+
533
+ if (!orchestration) {
534
+ return null;
535
+ }
536
+
537
+ return orchestration.agents.find(a => a.id === agentId) || null;
538
+ }
539
+
540
+ /**
541
+ * Update an agent within an orchestration
542
+ * @param {string} orchestrationId - Orchestration ID
543
+ * @param {string} agentId - Agent ID
544
+ * @param {Object} updates - Updates to apply
545
+ * @returns {Object} Result with updated agent or error
546
+ */
547
+ function updateAgent(orchestrationId, agentId, updates) {
548
+ const orchestration = getOrchestration(orchestrationId);
549
+
550
+ if (!orchestration) {
551
+ return { error: 'Orchestration not found' };
552
+ }
553
+
554
+ const agentIndex = orchestration.agents.findIndex(a => a.id === agentId);
555
+
556
+ if (agentIndex === -1) {
557
+ return { error: 'Agent not found' };
558
+ }
559
+
560
+ const agent = orchestration.agents[agentIndex];
561
+ const previousStatus = agent.status;
562
+
563
+ // Apply updates
564
+ if (updates.name !== undefined) agent.name = updates.name;
565
+ if (updates.status !== undefined) agent.status = updates.status;
566
+ if (updates.prompt !== undefined) agent.prompt = updates.prompt;
567
+ if (updates.sessionId !== undefined) agent.sessionId = updates.sessionId;
568
+ if (updates.tmuxWindow !== undefined) agent.tmuxWindow = updates.tmuxWindow;
569
+ if (updates.startedAt !== undefined) agent.startedAt = updates.startedAt;
570
+ if (updates.completedAt !== undefined) agent.completedAt = updates.completedAt;
571
+ if (updates.error !== undefined) agent.error = updates.error;
572
+ if (updates.output !== undefined) agent.output = updates.output;
573
+ if (updates.toolsUsed !== undefined) agent.toolsUsed = updates.toolsUsed;
574
+ if (updates.metadata !== undefined) {
575
+ agent.metadata = { ...agent.metadata, ...updates.metadata };
576
+ }
577
+
578
+ orchestration.agents[agentIndex] = agent;
579
+
580
+ try {
581
+ const filePath = getOrchestrationPath(orchestrationId);
582
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
583
+
584
+ eventBus.emit(EventTypes.AGENT_STATUS_CHANGED, {
585
+ orchestrationId,
586
+ agentId,
587
+ agent,
588
+ updates,
589
+ previousStatus
590
+ });
591
+
592
+ // Emit specific agent events
593
+ if (updates.status && updates.status !== previousStatus) {
594
+ switch (updates.status) {
595
+ case AGENT_STATUS.RUNNING:
596
+ eventBus.emit(EventTypes.AGENT_SPAWNED, { orchestrationId, agent });
597
+ break;
598
+ case AGENT_STATUS.COMPLETED:
599
+ eventBus.emit(EventTypes.AGENT_COMPLETED, { orchestrationId, agent });
600
+ break;
601
+ case AGENT_STATUS.FAILED:
602
+ eventBus.emit(EventTypes.AGENT_FAILED, { orchestrationId, agent });
603
+ break;
604
+ case AGENT_STATUS.CANCELLED:
605
+ eventBus.emit(EventTypes.AGENT_KILLED, { orchestrationId, agent });
606
+ break;
607
+ }
608
+ }
609
+
610
+ return { success: true, agent };
611
+ } catch (e) {
612
+ return { error: e.message };
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Remove an agent from an orchestration
618
+ * @param {string} orchestrationId - Orchestration ID
619
+ * @param {string} agentId - Agent ID
620
+ * @returns {Object} Result with success flag or error
621
+ */
622
+ function removeAgent(orchestrationId, agentId) {
623
+ const orchestration = getOrchestration(orchestrationId);
624
+
625
+ if (!orchestration) {
626
+ return { error: 'Orchestration not found' };
627
+ }
628
+
629
+ const agentIndex = orchestration.agents.findIndex(a => a.id === agentId);
630
+
631
+ if (agentIndex === -1) {
632
+ return { error: 'Agent not found' };
633
+ }
634
+
635
+ const agent = orchestration.agents[agentIndex];
636
+
637
+ // Check if agent is running - can't remove running agents
638
+ if (agent.status === AGENT_STATUS.RUNNING || agent.status === AGENT_STATUS.SPAWNING) {
639
+ return { error: 'Cannot remove a running agent. Kill it first.' };
640
+ }
641
+
642
+ // Remove this agent from dependencies of other agents
643
+ for (const otherAgent of orchestration.agents) {
644
+ if (otherAgent.dependencies.includes(agentId)) {
645
+ otherAgent.dependencies = otherAgent.dependencies.filter(id => id !== agentId);
646
+ }
647
+ }
648
+
649
+ orchestration.agents.splice(agentIndex, 1);
650
+
651
+ try {
652
+ const filePath = getOrchestrationPath(orchestrationId);
653
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
654
+
655
+ eventBus.emit(EventTypes.AGENT_KILLED, { orchestrationId, agent, removed: true });
656
+
657
+ return { success: true };
658
+ } catch (e) {
659
+ return { error: e.message };
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Add a dependency between agents
665
+ * @param {string} orchestrationId - Orchestration ID
666
+ * @param {string} agentId - Agent ID (the one that will be blocked)
667
+ * @param {string} dependsOnAgentId - Agent ID to depend on (the blocker)
668
+ * @returns {Object} Result with success flag or error
669
+ */
670
+ function addAgentDependency(orchestrationId, agentId, dependsOnAgentId) {
671
+ const orchestration = getOrchestration(orchestrationId);
672
+
673
+ if (!orchestration) {
674
+ return { error: 'Orchestration not found' };
675
+ }
676
+
677
+ const agent = orchestration.agents.find(a => a.id === agentId);
678
+ const dependsOnAgent = orchestration.agents.find(a => a.id === dependsOnAgentId);
679
+
680
+ if (!agent || !dependsOnAgent) {
681
+ return { error: 'Agent not found' };
682
+ }
683
+
684
+ if (agentId === dependsOnAgentId) {
685
+ return { error: 'Agent cannot depend on itself' };
686
+ }
687
+
688
+ // Check for circular dependencies
689
+ if (wouldCreateCycle(orchestration, agentId, dependsOnAgentId)) {
690
+ return { error: 'This would create a circular dependency' };
691
+ }
692
+
693
+ if (!agent.dependencies.includes(dependsOnAgentId)) {
694
+ agent.dependencies.push(dependsOnAgentId);
695
+
696
+ try {
697
+ const filePath = getOrchestrationPath(orchestrationId);
698
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
699
+
700
+ eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
701
+ orchestrationId,
702
+ orchestration,
703
+ updates: { dependencyAdded: { agentId, dependsOnAgentId } }
704
+ });
705
+
706
+ return { success: true };
707
+ } catch (e) {
708
+ return { error: e.message };
709
+ }
710
+ }
711
+
712
+ return { success: true }; // Already exists
713
+ }
714
+
715
+ /**
716
+ * Remove a dependency between agents
717
+ * @param {string} orchestrationId - Orchestration ID
718
+ * @param {string} agentId - Agent ID
719
+ * @param {string} dependsOnAgentId - Agent ID to remove from dependencies
720
+ * @returns {Object} Result with success flag or error
721
+ */
722
+ function removeAgentDependency(orchestrationId, agentId, dependsOnAgentId) {
723
+ const orchestration = getOrchestration(orchestrationId);
724
+
725
+ if (!orchestration) {
726
+ return { error: 'Orchestration not found' };
727
+ }
728
+
729
+ const agent = orchestration.agents.find(a => a.id === agentId);
730
+
731
+ if (!agent) {
732
+ return { error: 'Agent not found' };
733
+ }
734
+
735
+ agent.dependencies = agent.dependencies.filter(id => id !== dependsOnAgentId);
736
+
737
+ try {
738
+ const filePath = getOrchestrationPath(orchestrationId);
739
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
740
+
741
+ eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
742
+ orchestrationId,
743
+ orchestration,
744
+ updates: { dependencyRemoved: { agentId, dependsOnAgentId } }
745
+ });
746
+
747
+ return { success: true };
748
+ } catch (e) {
749
+ return { error: e.message };
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Check if adding a dependency would create a cycle
755
+ * @param {Object} orchestration - Orchestration object
756
+ * @param {string} agentId - Agent that would get the dependency
757
+ * @param {string} dependsOnAgentId - The dependency to add
758
+ * @returns {boolean} True if it would create a cycle
759
+ */
760
+ function wouldCreateCycle(orchestration, agentId, dependsOnAgentId) {
761
+ const visited = new Set();
762
+ const stack = [dependsOnAgentId];
763
+
764
+ while (stack.length > 0) {
765
+ const current = stack.pop();
766
+
767
+ if (current === agentId) {
768
+ return true; // Found a cycle
769
+ }
770
+
771
+ if (visited.has(current)) {
772
+ continue;
773
+ }
774
+
775
+ visited.add(current);
776
+
777
+ const agent = orchestration.agents.find(a => a.id === current);
778
+ if (agent) {
779
+ stack.push(...agent.dependencies);
780
+ }
781
+ }
782
+
783
+ return false;
784
+ }
785
+
786
+ /**
787
+ * Get agents that are ready to run (no pending dependencies)
788
+ * @param {string} orchestrationId - Orchestration ID
789
+ * @returns {Array} Array of agents ready to run
790
+ */
791
+ function getReadyAgents(orchestrationId) {
792
+ const orchestration = getOrchestration(orchestrationId);
793
+
794
+ if (!orchestration) {
795
+ return [];
796
+ }
797
+
798
+ return orchestration.agents.filter(agent => {
799
+ // Only pending agents can be ready
800
+ if (agent.status !== AGENT_STATUS.PENDING) {
801
+ return false;
802
+ }
803
+
804
+ // Check all dependencies are completed
805
+ for (const depId of agent.dependencies) {
806
+ const depAgent = orchestration.agents.find(a => a.id === depId);
807
+ if (!depAgent || depAgent.status !== AGENT_STATUS.COMPLETED) {
808
+ return false;
809
+ }
810
+ }
811
+
812
+ return true;
813
+ });
814
+ }
815
+
816
+ /**
817
+ * Get orchestration statistics
818
+ * @param {string} orchestrationId - Orchestration ID
819
+ * @returns {Object} Statistics object
820
+ */
821
+ function getOrchestrationStats(orchestrationId) {
822
+ const orchestration = getOrchestration(orchestrationId);
823
+
824
+ if (!orchestration) {
825
+ return null;
826
+ }
827
+
828
+ const agents = orchestration.agents;
829
+
830
+ return {
831
+ total: agents.length,
832
+ pending: agents.filter(a => a.status === AGENT_STATUS.PENDING).length,
833
+ spawning: agents.filter(a => a.status === AGENT_STATUS.SPAWNING).length,
834
+ running: agents.filter(a => a.status === AGENT_STATUS.RUNNING).length,
835
+ waiting: agents.filter(a => a.status === AGENT_STATUS.WAITING).length,
836
+ completed: agents.filter(a => a.status === AGENT_STATUS.COMPLETED).length,
837
+ failed: agents.filter(a => a.status === AGENT_STATUS.FAILED).length,
838
+ cancelled: agents.filter(a => a.status === AGENT_STATUS.CANCELLED).length,
839
+ readyToRun: getReadyAgents(orchestrationId).length
840
+ };
841
+ }
842
+
843
+ /**
844
+ * Check if orchestration is complete (all agents done)
845
+ * @param {string} orchestrationId - Orchestration ID
846
+ * @returns {boolean} True if complete
847
+ */
848
+ function isOrchestrationComplete(orchestrationId) {
849
+ const orchestration = getOrchestration(orchestrationId);
850
+
851
+ if (!orchestration || orchestration.agents.length === 0) {
852
+ return false;
853
+ }
854
+
855
+ const terminalStatuses = [
856
+ AGENT_STATUS.COMPLETED,
857
+ AGENT_STATUS.FAILED,
858
+ AGENT_STATUS.CANCELLED
859
+ ];
860
+
861
+ return orchestration.agents.every(a => terminalStatuses.includes(a.status));
862
+ }
863
+
864
+ /**
865
+ * Record tool usage for an agent
866
+ * @param {string} orchestrationId - Orchestration ID
867
+ * @param {string} agentId - Agent ID
868
+ * @param {Object} toolData - Tool usage data
869
+ * @returns {Object} Result with success flag
870
+ */
871
+ function recordAgentToolUsage(orchestrationId, agentId, toolData) {
872
+ const orchestration = getOrchestration(orchestrationId);
873
+
874
+ if (!orchestration) {
875
+ return { error: 'Orchestration not found' };
876
+ }
877
+
878
+ const agent = orchestration.agents.find(a => a.id === agentId);
879
+
880
+ if (!agent) {
881
+ return { error: 'Agent not found' };
882
+ }
883
+
884
+ agent.toolsUsed.push({
885
+ tool: toolData.tool,
886
+ timestamp: new Date().toISOString(),
887
+ duration: toolData.duration || null,
888
+ status: toolData.status || 'unknown'
889
+ });
890
+
891
+ try {
892
+ const filePath = getOrchestrationPath(orchestrationId);
893
+ fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
894
+
895
+ eventBus.emit(EventTypes.AGENT_OUTPUT, {
896
+ orchestrationId,
897
+ agentId,
898
+ type: 'tool_use',
899
+ data: toolData
900
+ });
901
+
902
+ return { success: true };
903
+ } catch (e) {
904
+ return { error: e.message };
905
+ }
906
+ }
907
+
908
+ module.exports = {
909
+ // Orchestration CRUD
910
+ createOrchestration,
911
+ getOrchestration,
912
+ listOrchestrations,
913
+ updateOrchestration,
914
+ deleteOrchestration,
915
+
916
+ // Agent CRUD
917
+ addAgent,
918
+ getAgent,
919
+ updateAgent,
920
+ removeAgent,
921
+
922
+ // Dependencies
923
+ addAgentDependency,
924
+ removeAgentDependency,
925
+ getReadyAgents,
926
+
927
+ // Templates
928
+ getTemplates,
929
+ getTemplate,
930
+ createFromTemplate,
931
+
932
+ // Utilities
933
+ getOrchestrationStats,
934
+ isOrchestrationComplete,
935
+ recordAgentToolUsage,
936
+ generateId,
937
+
938
+ // Constants re-export for convenience
939
+ AGENT_STATUS,
940
+ ORCHESTRATION_STATUS
941
+ };