@sparkleideas/swarm 3.0.0-alpha.7
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/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Task Orchestrator
|
|
3
|
+
* Manages task decomposition, dependency resolution, and parallel execution
|
|
4
|
+
*
|
|
5
|
+
* Based on ADR-003 (Single Coordination Engine) and ADR-001 (@sparkleideas/agentic-flow integration)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
TaskId,
|
|
10
|
+
TaskType,
|
|
11
|
+
TaskStatus,
|
|
12
|
+
TaskPriority,
|
|
13
|
+
TaskDefinition,
|
|
14
|
+
TaskMetadata,
|
|
15
|
+
TaskResult,
|
|
16
|
+
TaskResultMetrics,
|
|
17
|
+
AgentId,
|
|
18
|
+
AgentDomain,
|
|
19
|
+
PhaseId,
|
|
20
|
+
SwarmEvent
|
|
21
|
+
} from '../shared/types';
|
|
22
|
+
import {
|
|
23
|
+
IEventBus,
|
|
24
|
+
taskCreatedEvent,
|
|
25
|
+
taskQueuedEvent,
|
|
26
|
+
taskAssignedEvent,
|
|
27
|
+
taskStartedEvent,
|
|
28
|
+
taskCompletedEvent,
|
|
29
|
+
taskFailedEvent,
|
|
30
|
+
taskBlockedEvent
|
|
31
|
+
} from '../shared/events';
|
|
32
|
+
import { IAgentRegistry } from './agent-registry';
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Task Orchestrator Interface
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export interface ITaskOrchestrator {
|
|
39
|
+
// Task Creation
|
|
40
|
+
createTask(spec: TaskSpec): TaskDefinition;
|
|
41
|
+
createBatchTasks(specs: TaskSpec[]): TaskDefinition[];
|
|
42
|
+
|
|
43
|
+
// Task Lifecycle
|
|
44
|
+
queueTask(taskId: TaskId): void;
|
|
45
|
+
assignTask(taskId: TaskId, agentId: AgentId): void;
|
|
46
|
+
startTask(taskId: TaskId): void;
|
|
47
|
+
completeTask(taskId: TaskId, result: TaskResult): void;
|
|
48
|
+
failTask(taskId: TaskId, error: Error): void;
|
|
49
|
+
cancelTask(taskId: TaskId): void;
|
|
50
|
+
|
|
51
|
+
// Dependency Management
|
|
52
|
+
addDependency(taskId: TaskId, dependsOn: TaskId): void;
|
|
53
|
+
removeDependency(taskId: TaskId, dependsOn: TaskId): void;
|
|
54
|
+
getDependencies(taskId: TaskId): TaskId[];
|
|
55
|
+
getDependents(taskId: TaskId): TaskId[];
|
|
56
|
+
isBlocked(taskId: TaskId): boolean;
|
|
57
|
+
getBlockingTasks(taskId: TaskId): TaskId[];
|
|
58
|
+
|
|
59
|
+
// Queries
|
|
60
|
+
getTask(taskId: TaskId): TaskDefinition | undefined;
|
|
61
|
+
getAllTasks(): TaskDefinition[];
|
|
62
|
+
getTasksByStatus(status: TaskStatus): TaskDefinition[];
|
|
63
|
+
getTasksByAgent(agentId: AgentId): TaskDefinition[];
|
|
64
|
+
getTasksByDomain(domain: AgentDomain): TaskDefinition[];
|
|
65
|
+
getTasksByPhase(phase: PhaseId): TaskDefinition[];
|
|
66
|
+
|
|
67
|
+
// Queue Management
|
|
68
|
+
getNextTask(agentId?: AgentId): TaskDefinition | undefined;
|
|
69
|
+
getPriorityQueue(): TaskDefinition[];
|
|
70
|
+
|
|
71
|
+
// Metrics
|
|
72
|
+
getTaskMetrics(): TaskOrchestratorMetrics;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TaskSpec {
|
|
76
|
+
type: TaskType;
|
|
77
|
+
title: string;
|
|
78
|
+
description: string;
|
|
79
|
+
priority?: TaskPriority;
|
|
80
|
+
dependencies?: TaskId[];
|
|
81
|
+
domain: AgentDomain;
|
|
82
|
+
phase: PhaseId;
|
|
83
|
+
estimatedDuration?: number;
|
|
84
|
+
tags?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TaskOrchestratorMetrics {
|
|
88
|
+
totalTasks: number;
|
|
89
|
+
tasksByStatus: Record<TaskStatus, number>;
|
|
90
|
+
tasksByPriority: Record<TaskPriority, number>;
|
|
91
|
+
tasksByDomain: Record<AgentDomain, number>;
|
|
92
|
+
averageQueueTime: number;
|
|
93
|
+
averageExecutionTime: number;
|
|
94
|
+
throughput: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// Task Orchestrator Implementation
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
export class TaskOrchestrator implements ITaskOrchestrator {
|
|
102
|
+
private tasks: Map<TaskId, TaskDefinition> = new Map();
|
|
103
|
+
private dependencyGraph: Map<TaskId, Set<TaskId>> = new Map();
|
|
104
|
+
private dependentGraph: Map<TaskId, Set<TaskId>> = new Map();
|
|
105
|
+
private taskCounter: number = 0;
|
|
106
|
+
private eventBus: IEventBus;
|
|
107
|
+
private agentRegistry: IAgentRegistry;
|
|
108
|
+
|
|
109
|
+
constructor(eventBus: IEventBus, agentRegistry: IAgentRegistry) {
|
|
110
|
+
this.eventBus = eventBus;
|
|
111
|
+
this.agentRegistry = agentRegistry;
|
|
112
|
+
|
|
113
|
+
this.eventBus.subscribe('agent:task-completed', this.handleAgentTaskCompleted.bind(this));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==========================================================================
|
|
117
|
+
// Task Creation
|
|
118
|
+
// ==========================================================================
|
|
119
|
+
|
|
120
|
+
createTask(spec: TaskSpec): TaskDefinition {
|
|
121
|
+
const taskId: TaskId = `task-${Date.now()}-${++this.taskCounter}`;
|
|
122
|
+
|
|
123
|
+
const task: TaskDefinition = {
|
|
124
|
+
id: taskId,
|
|
125
|
+
type: spec.type,
|
|
126
|
+
title: spec.title,
|
|
127
|
+
description: spec.description,
|
|
128
|
+
assignedAgent: null,
|
|
129
|
+
status: 'pending',
|
|
130
|
+
priority: spec.priority ?? 'medium',
|
|
131
|
+
dependencies: spec.dependencies ?? [],
|
|
132
|
+
blockedBy: [],
|
|
133
|
+
metadata: {
|
|
134
|
+
domain: spec.domain,
|
|
135
|
+
phase: spec.phase,
|
|
136
|
+
estimatedDuration: spec.estimatedDuration ?? 0,
|
|
137
|
+
actualDuration: null,
|
|
138
|
+
retryCount: 0,
|
|
139
|
+
maxRetries: 3,
|
|
140
|
+
artifacts: [],
|
|
141
|
+
tags: spec.tags ?? []
|
|
142
|
+
},
|
|
143
|
+
createdAt: Date.now(),
|
|
144
|
+
updatedAt: Date.now(),
|
|
145
|
+
completedAt: null
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.tasks.set(taskId, task);
|
|
149
|
+
this.dependencyGraph.set(taskId, new Set(task.dependencies));
|
|
150
|
+
this.dependentGraph.set(taskId, new Set());
|
|
151
|
+
|
|
152
|
+
for (const depId of task.dependencies) {
|
|
153
|
+
if (this.dependentGraph.has(depId)) {
|
|
154
|
+
this.dependentGraph.get(depId)!.add(taskId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.updateBlockedStatus(taskId);
|
|
159
|
+
|
|
160
|
+
this.eventBus.emitSync(taskCreatedEvent(taskId, { type: spec.type, title: spec.title }));
|
|
161
|
+
|
|
162
|
+
return task;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
createBatchTasks(specs: TaskSpec[]): TaskDefinition[] {
|
|
166
|
+
return specs.map(spec => this.createTask(spec));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ==========================================================================
|
|
170
|
+
// Task Lifecycle
|
|
171
|
+
// ==========================================================================
|
|
172
|
+
|
|
173
|
+
queueTask(taskId: TaskId): void {
|
|
174
|
+
const task = this.getTaskOrThrow(taskId);
|
|
175
|
+
|
|
176
|
+
if (this.isBlocked(taskId)) {
|
|
177
|
+
this.updateTaskStatus(taskId, 'blocked');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.updateTaskStatus(taskId, 'queued');
|
|
182
|
+
this.eventBus.emitSync(taskQueuedEvent(taskId, this.getQueuePosition(taskId)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
assignTask(taskId: TaskId, agentId: AgentId): void {
|
|
186
|
+
const task = this.getTaskOrThrow(taskId);
|
|
187
|
+
|
|
188
|
+
if (task.status !== 'queued') {
|
|
189
|
+
throw new Error(`Task ${taskId} is not queued (status: ${task.status})`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.isBlocked(taskId)) {
|
|
193
|
+
throw new Error(`Task ${taskId} is blocked by dependencies`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
task.assignedAgent = agentId;
|
|
197
|
+
this.updateTaskStatus(taskId, 'assigned');
|
|
198
|
+
|
|
199
|
+
this.agentRegistry.assignTask(agentId, taskId);
|
|
200
|
+
|
|
201
|
+
this.eventBus.emitSync(taskAssignedEvent(taskId, agentId));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
startTask(taskId: TaskId): void {
|
|
205
|
+
const task = this.getTaskOrThrow(taskId);
|
|
206
|
+
|
|
207
|
+
if (task.status !== 'assigned') {
|
|
208
|
+
throw new Error(`Task ${taskId} is not assigned (status: ${task.status})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.updateTaskStatus(taskId, 'in-progress');
|
|
212
|
+
|
|
213
|
+
if (task.assignedAgent) {
|
|
214
|
+
this.eventBus.emitSync(taskStartedEvent(taskId, task.assignedAgent));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
completeTask(taskId: TaskId, result: TaskResult): void {
|
|
219
|
+
const task = this.getTaskOrThrow(taskId);
|
|
220
|
+
|
|
221
|
+
if (task.status !== 'in-progress') {
|
|
222
|
+
throw new Error(`Task ${taskId} is not in progress (status: ${task.status})`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
task.metadata.actualDuration = result.duration;
|
|
226
|
+
task.completedAt = Date.now();
|
|
227
|
+
|
|
228
|
+
this.updateTaskStatus(taskId, 'completed');
|
|
229
|
+
|
|
230
|
+
if (task.assignedAgent) {
|
|
231
|
+
this.agentRegistry.completeTask(task.assignedAgent, taskId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.eventBus.emitSync(taskCompletedEvent(taskId, result));
|
|
235
|
+
|
|
236
|
+
this.unblockDependentTasks(taskId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
failTask(taskId: TaskId, error: Error): void {
|
|
240
|
+
const task = this.getTaskOrThrow(taskId);
|
|
241
|
+
|
|
242
|
+
task.metadata.retryCount++;
|
|
243
|
+
|
|
244
|
+
if (task.metadata.retryCount < task.metadata.maxRetries) {
|
|
245
|
+
this.updateTaskStatus(taskId, 'queued');
|
|
246
|
+
task.assignedAgent = null;
|
|
247
|
+
} else {
|
|
248
|
+
this.updateTaskStatus(taskId, 'failed');
|
|
249
|
+
|
|
250
|
+
if (task.assignedAgent) {
|
|
251
|
+
const agentState = this.agentRegistry.getState(task.assignedAgent);
|
|
252
|
+
if (agentState) {
|
|
253
|
+
agentState.metrics.tasksFailed++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.eventBus.emitSync(taskFailedEvent(taskId, error));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cancelTask(taskId: TaskId): void {
|
|
262
|
+
const task = this.getTaskOrThrow(taskId);
|
|
263
|
+
|
|
264
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
265
|
+
throw new Error(`Cannot cancel ${task.status} task ${taskId}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (task.assignedAgent) {
|
|
269
|
+
this.agentRegistry.updateStatus(task.assignedAgent, 'idle');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.updateTaskStatus(taskId, 'cancelled');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ==========================================================================
|
|
276
|
+
// Dependency Management
|
|
277
|
+
// ==========================================================================
|
|
278
|
+
|
|
279
|
+
addDependency(taskId: TaskId, dependsOn: TaskId): void {
|
|
280
|
+
const task = this.getTaskOrThrow(taskId);
|
|
281
|
+
this.getTaskOrThrow(dependsOn);
|
|
282
|
+
|
|
283
|
+
if (this.wouldCreateCycle(taskId, dependsOn)) {
|
|
284
|
+
throw new Error(`Adding dependency ${dependsOn} to ${taskId} would create a cycle`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
task.dependencies.push(dependsOn);
|
|
288
|
+
this.dependencyGraph.get(taskId)!.add(dependsOn);
|
|
289
|
+
this.dependentGraph.get(dependsOn)!.add(taskId);
|
|
290
|
+
|
|
291
|
+
this.updateBlockedStatus(taskId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
removeDependency(taskId: TaskId, dependsOn: TaskId): void {
|
|
295
|
+
const task = this.getTaskOrThrow(taskId);
|
|
296
|
+
|
|
297
|
+
const index = task.dependencies.indexOf(dependsOn);
|
|
298
|
+
if (index > -1) {
|
|
299
|
+
task.dependencies.splice(index, 1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.dependencyGraph.get(taskId)?.delete(dependsOn);
|
|
303
|
+
this.dependentGraph.get(dependsOn)?.delete(taskId);
|
|
304
|
+
|
|
305
|
+
this.updateBlockedStatus(taskId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getDependencies(taskId: TaskId): TaskId[] {
|
|
309
|
+
return Array.from(this.dependencyGraph.get(taskId) ?? []);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getDependents(taskId: TaskId): TaskId[] {
|
|
313
|
+
return Array.from(this.dependentGraph.get(taskId) ?? []);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
isBlocked(taskId: TaskId): boolean {
|
|
317
|
+
const blockingTasks = this.getBlockingTasks(taskId);
|
|
318
|
+
return blockingTasks.length > 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
getBlockingTasks(taskId: TaskId): TaskId[] {
|
|
322
|
+
const dependencies = this.dependencyGraph.get(taskId);
|
|
323
|
+
if (!dependencies) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return Array.from(dependencies).filter(depId => {
|
|
328
|
+
const depTask = this.tasks.get(depId);
|
|
329
|
+
return depTask && depTask.status !== 'completed';
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ==========================================================================
|
|
334
|
+
// Queries
|
|
335
|
+
// ==========================================================================
|
|
336
|
+
|
|
337
|
+
getTask(taskId: TaskId): TaskDefinition | undefined {
|
|
338
|
+
return this.tasks.get(taskId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
getAllTasks(): TaskDefinition[] {
|
|
342
|
+
return Array.from(this.tasks.values());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
getTasksByStatus(status: TaskStatus): TaskDefinition[] {
|
|
346
|
+
return this.getAllTasks().filter(t => t.status === status);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getTasksByAgent(agentId: AgentId): TaskDefinition[] {
|
|
350
|
+
return this.getAllTasks().filter(t => t.assignedAgent === agentId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getTasksByDomain(domain: AgentDomain): TaskDefinition[] {
|
|
354
|
+
return this.getAllTasks().filter(t => t.metadata.domain === domain);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
getTasksByPhase(phase: PhaseId): TaskDefinition[] {
|
|
358
|
+
return this.getAllTasks().filter(t => t.metadata.phase === phase);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ==========================================================================
|
|
362
|
+
// Queue Management
|
|
363
|
+
// ==========================================================================
|
|
364
|
+
|
|
365
|
+
getNextTask(agentId?: AgentId): TaskDefinition | undefined {
|
|
366
|
+
const queuedTasks = this.getTasksByStatus('queued')
|
|
367
|
+
.filter(t => !this.isBlocked(t.id))
|
|
368
|
+
.sort((a, b) => {
|
|
369
|
+
const priorityOrder: Record<TaskPriority, number> = {
|
|
370
|
+
'critical': 0,
|
|
371
|
+
'high': 1,
|
|
372
|
+
'medium': 2,
|
|
373
|
+
'low': 3
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
377
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
378
|
+
|
|
379
|
+
return a.createdAt - b.createdAt;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!agentId) {
|
|
383
|
+
return queuedTasks[0];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const agentDef = this.agentRegistry.getDefinition(agentId);
|
|
387
|
+
if (!agentDef) {
|
|
388
|
+
return queuedTasks[0];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const supportedTypes = agentDef.capabilities.flatMap(c => c.supportedTaskTypes);
|
|
392
|
+
|
|
393
|
+
return queuedTasks.find(t => supportedTypes.includes(t.type));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getPriorityQueue(): TaskDefinition[] {
|
|
397
|
+
return this.getTasksByStatus('queued')
|
|
398
|
+
.filter(t => !this.isBlocked(t.id))
|
|
399
|
+
.sort((a, b) => {
|
|
400
|
+
const priorityOrder: Record<TaskPriority, number> = {
|
|
401
|
+
'critical': 0,
|
|
402
|
+
'high': 1,
|
|
403
|
+
'medium': 2,
|
|
404
|
+
'low': 3
|
|
405
|
+
};
|
|
406
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ==========================================================================
|
|
411
|
+
// Metrics
|
|
412
|
+
// ==========================================================================
|
|
413
|
+
|
|
414
|
+
getTaskMetrics(): TaskOrchestratorMetrics {
|
|
415
|
+
const allTasks = this.getAllTasks();
|
|
416
|
+
|
|
417
|
+
const tasksByStatus: Record<TaskStatus, number> = {
|
|
418
|
+
'pending': 0,
|
|
419
|
+
'queued': 0,
|
|
420
|
+
'assigned': 0,
|
|
421
|
+
'in-progress': 0,
|
|
422
|
+
'blocked': 0,
|
|
423
|
+
'completed': 0,
|
|
424
|
+
'failed': 0,
|
|
425
|
+
'cancelled': 0
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const tasksByPriority: Record<TaskPriority, number> = {
|
|
429
|
+
'critical': 0,
|
|
430
|
+
'high': 0,
|
|
431
|
+
'medium': 0,
|
|
432
|
+
'low': 0
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const tasksByDomain: Record<AgentDomain, number> = {
|
|
436
|
+
'security': 0,
|
|
437
|
+
'core': 0,
|
|
438
|
+
'integration': 0,
|
|
439
|
+
'quality': 0,
|
|
440
|
+
'performance': 0,
|
|
441
|
+
'deployment': 0
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
let totalQueueTime = 0;
|
|
445
|
+
let totalExecutionTime = 0;
|
|
446
|
+
let completedCount = 0;
|
|
447
|
+
|
|
448
|
+
for (const task of allTasks) {
|
|
449
|
+
tasksByStatus[task.status]++;
|
|
450
|
+
tasksByPriority[task.priority]++;
|
|
451
|
+
tasksByDomain[task.metadata.domain]++;
|
|
452
|
+
|
|
453
|
+
if (task.status === 'completed' && task.metadata.actualDuration) {
|
|
454
|
+
totalExecutionTime += task.metadata.actualDuration;
|
|
455
|
+
completedCount++;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const firstTask = allTasks[0];
|
|
461
|
+
const runtimeMs = firstTask ? now - firstTask.createdAt : 1;
|
|
462
|
+
const throughput = completedCount / (runtimeMs / 1000 / 60);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
totalTasks: allTasks.length,
|
|
466
|
+
tasksByStatus,
|
|
467
|
+
tasksByPriority,
|
|
468
|
+
tasksByDomain,
|
|
469
|
+
averageQueueTime: totalQueueTime / (completedCount || 1),
|
|
470
|
+
averageExecutionTime: totalExecutionTime / (completedCount || 1),
|
|
471
|
+
throughput
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ==========================================================================
|
|
476
|
+
// Private Helpers
|
|
477
|
+
// ==========================================================================
|
|
478
|
+
|
|
479
|
+
private getTaskOrThrow(taskId: TaskId): TaskDefinition {
|
|
480
|
+
const task = this.tasks.get(taskId);
|
|
481
|
+
if (!task) {
|
|
482
|
+
throw new Error(`Task ${taskId} not found`);
|
|
483
|
+
}
|
|
484
|
+
return task;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private updateTaskStatus(taskId: TaskId, status: TaskStatus): void {
|
|
488
|
+
const task = this.getTaskOrThrow(taskId);
|
|
489
|
+
task.status = status;
|
|
490
|
+
task.updatedAt = Date.now();
|
|
491
|
+
|
|
492
|
+
if (status === 'blocked') {
|
|
493
|
+
task.blockedBy = this.getBlockingTasks(taskId);
|
|
494
|
+
} else {
|
|
495
|
+
task.blockedBy = [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private updateBlockedStatus(taskId: TaskId): void {
|
|
500
|
+
const task = this.tasks.get(taskId);
|
|
501
|
+
if (!task || task.status === 'completed' || task.status === 'failed') {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const isBlocked = this.isBlocked(taskId);
|
|
506
|
+
|
|
507
|
+
if (isBlocked && task.status !== 'blocked') {
|
|
508
|
+
this.updateTaskStatus(taskId, 'blocked');
|
|
509
|
+
const blockingTasks = this.getBlockingTasks(taskId);
|
|
510
|
+
this.eventBus.emitSync(taskBlockedEvent(taskId, 'Blocked by dependencies', blockingTasks[0]));
|
|
511
|
+
} else if (!isBlocked && task.status === 'blocked') {
|
|
512
|
+
this.updateTaskStatus(taskId, 'queued');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private unblockDependentTasks(taskId: TaskId): void {
|
|
517
|
+
const dependents = this.getDependents(taskId);
|
|
518
|
+
|
|
519
|
+
for (const dependentId of dependents) {
|
|
520
|
+
this.updateBlockedStatus(dependentId);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private wouldCreateCycle(taskId: TaskId, newDependency: TaskId): boolean {
|
|
525
|
+
const visited = new Set<TaskId>();
|
|
526
|
+
const stack = [newDependency];
|
|
527
|
+
|
|
528
|
+
while (stack.length > 0) {
|
|
529
|
+
const current = stack.pop()!;
|
|
530
|
+
|
|
531
|
+
if (current === taskId) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (visited.has(current)) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
visited.add(current);
|
|
540
|
+
|
|
541
|
+
const deps = this.dependencyGraph.get(current);
|
|
542
|
+
if (deps) {
|
|
543
|
+
stack.push(...deps);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private handleAgentTaskCompleted(event: SwarmEvent): void {
|
|
551
|
+
const payload = event.payload as { taskId: TaskId; result: unknown };
|
|
552
|
+
|
|
553
|
+
if (payload.taskId && this.tasks.has(payload.taskId)) {
|
|
554
|
+
const task = this.tasks.get(payload.taskId)!;
|
|
555
|
+
if (task.status === 'in-progress') {
|
|
556
|
+
this.completeTask(payload.taskId, {
|
|
557
|
+
taskId: payload.taskId,
|
|
558
|
+
success: true,
|
|
559
|
+
output: payload.result,
|
|
560
|
+
error: null,
|
|
561
|
+
duration: Date.now() - task.updatedAt,
|
|
562
|
+
metrics: this.createDefaultMetrics()
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private createDefaultMetrics(): TaskResultMetrics {
|
|
569
|
+
return {
|
|
570
|
+
linesOfCode: 0,
|
|
571
|
+
testsWritten: 0,
|
|
572
|
+
testsPassed: 0,
|
|
573
|
+
coveragePercent: 0,
|
|
574
|
+
performanceImpact: 0
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private getQueuePosition(taskId: TaskId): number {
|
|
579
|
+
const priorityOrder: Record<string, number> = {
|
|
580
|
+
critical: 5,
|
|
581
|
+
high: 4,
|
|
582
|
+
normal: 3,
|
|
583
|
+
low: 2,
|
|
584
|
+
background: 1,
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const queuedTasks = Array.from(this.tasks.values())
|
|
588
|
+
.filter(t => t.status === 'queued')
|
|
589
|
+
.sort((a, b) => (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0));
|
|
590
|
+
|
|
591
|
+
const position = queuedTasks.findIndex(t => t.id === taskId);
|
|
592
|
+
return position >= 0 ? position + 1 : queuedTasks.length + 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// =============================================================================
|
|
597
|
+
// Factory Function
|
|
598
|
+
// =============================================================================
|
|
599
|
+
|
|
600
|
+
export function createTaskOrchestrator(
|
|
601
|
+
eventBus: IEventBus,
|
|
602
|
+
agentRegistry: IAgentRegistry
|
|
603
|
+
): ITaskOrchestrator {
|
|
604
|
+
return new TaskOrchestrator(eventBus, agentRegistry);
|
|
605
|
+
}
|