attocode 0.1.9 → 0.2.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +121 -1
  2. package/dist/src/adapters.d.ts.map +1 -1
  3. package/dist/src/adapters.js +1 -0
  4. package/dist/src/adapters.js.map +1 -1
  5. package/dist/src/agent.d.ts +5 -0
  6. package/dist/src/agent.d.ts.map +1 -1
  7. package/dist/src/agent.js +138 -31
  8. package/dist/src/agent.js.map +1 -1
  9. package/dist/src/cli.d.ts +6 -0
  10. package/dist/src/cli.d.ts.map +1 -1
  11. package/dist/src/cli.js +37 -0
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/commands/init-commands.d.ts.map +1 -1
  14. package/dist/src/commands/init-commands.js +57 -0
  15. package/dist/src/commands/init-commands.js.map +1 -1
  16. package/dist/src/core/protocol/types.d.ts +14 -14
  17. package/dist/src/defaults.d.ts.map +1 -1
  18. package/dist/src/defaults.js +1 -0
  19. package/dist/src/defaults.js.map +1 -1
  20. package/dist/src/integrations/economics.d.ts +9 -0
  21. package/dist/src/integrations/economics.d.ts.map +1 -1
  22. package/dist/src/integrations/economics.js +25 -0
  23. package/dist/src/integrations/economics.js.map +1 -1
  24. package/dist/src/integrations/index.d.ts +2 -1
  25. package/dist/src/integrations/index.d.ts.map +1 -1
  26. package/dist/src/integrations/index.js +3 -1
  27. package/dist/src/integrations/index.js.map +1 -1
  28. package/dist/src/integrations/learning-store.d.ts.map +1 -1
  29. package/dist/src/integrations/learning-store.js +6 -0
  30. package/dist/src/integrations/learning-store.js.map +1 -1
  31. package/dist/src/integrations/smart-decomposer.d.ts.map +1 -1
  32. package/dist/src/integrations/smart-decomposer.js +7 -0
  33. package/dist/src/integrations/smart-decomposer.js.map +1 -1
  34. package/dist/src/integrations/swarm/index.d.ts +29 -0
  35. package/dist/src/integrations/swarm/index.d.ts.map +1 -0
  36. package/dist/src/integrations/swarm/index.js +29 -0
  37. package/dist/src/integrations/swarm/index.js.map +1 -0
  38. package/dist/src/integrations/swarm/model-selector.d.ts +55 -0
  39. package/dist/src/integrations/swarm/model-selector.d.ts.map +1 -0
  40. package/dist/src/integrations/swarm/model-selector.js +342 -0
  41. package/dist/src/integrations/swarm/model-selector.js.map +1 -0
  42. package/dist/src/integrations/swarm/request-throttle.d.ts +112 -0
  43. package/dist/src/integrations/swarm/request-throttle.d.ts.map +1 -0
  44. package/dist/src/integrations/swarm/request-throttle.js +263 -0
  45. package/dist/src/integrations/swarm/request-throttle.js.map +1 -0
  46. package/dist/src/integrations/swarm/swarm-budget.d.ts +31 -0
  47. package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -0
  48. package/dist/src/integrations/swarm/swarm-budget.js +36 -0
  49. package/dist/src/integrations/swarm/swarm-budget.js.map +1 -0
  50. package/dist/src/integrations/swarm/swarm-config-loader.d.ts +51 -0
  51. package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -0
  52. package/dist/src/integrations/swarm/swarm-config-loader.js +458 -0
  53. package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -0
  54. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts +145 -0
  55. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts.map +1 -0
  56. package/dist/src/integrations/swarm/swarm-event-bridge.js +443 -0
  57. package/dist/src/integrations/swarm/swarm-event-bridge.js.map +1 -0
  58. package/dist/src/integrations/swarm/swarm-events.d.ts +157 -0
  59. package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -0
  60. package/dist/src/integrations/swarm/swarm-events.js +81 -0
  61. package/dist/src/integrations/swarm/swarm-events.js.map +1 -0
  62. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +166 -0
  63. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -0
  64. package/dist/src/integrations/swarm/swarm-orchestrator.js +1114 -0
  65. package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -0
  66. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts +29 -0
  67. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts.map +1 -0
  68. package/dist/src/integrations/swarm/swarm-quality-gate.js +85 -0
  69. package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -0
  70. package/dist/src/integrations/swarm/swarm-state-store.d.ts +31 -0
  71. package/dist/src/integrations/swarm/swarm-state-store.d.ts.map +1 -0
  72. package/dist/src/integrations/swarm/swarm-state-store.js +91 -0
  73. package/dist/src/integrations/swarm/swarm-state-store.js.map +1 -0
  74. package/dist/src/integrations/swarm/task-queue.d.ts +128 -0
  75. package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -0
  76. package/dist/src/integrations/swarm/task-queue.js +379 -0
  77. package/dist/src/integrations/swarm/task-queue.js.map +1 -0
  78. package/dist/src/integrations/swarm/types.d.ts +425 -0
  79. package/dist/src/integrations/swarm/types.d.ts.map +1 -0
  80. package/dist/src/integrations/swarm/types.js +96 -0
  81. package/dist/src/integrations/swarm/types.js.map +1 -0
  82. package/dist/src/integrations/swarm/worker-pool.d.ts +96 -0
  83. package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -0
  84. package/dist/src/integrations/swarm/worker-pool.js +269 -0
  85. package/dist/src/integrations/swarm/worker-pool.js.map +1 -0
  86. package/dist/src/main.js +88 -0
  87. package/dist/src/main.js.map +1 -1
  88. package/dist/src/modes/repl.d.ts +1 -0
  89. package/dist/src/modes/repl.d.ts.map +1 -1
  90. package/dist/src/modes/repl.js +2 -1
  91. package/dist/src/modes/repl.js.map +1 -1
  92. package/dist/src/modes/tui.d.ts +1 -0
  93. package/dist/src/modes/tui.d.ts.map +1 -1
  94. package/dist/src/modes/tui.js +3 -1
  95. package/dist/src/modes/tui.js.map +1 -1
  96. package/dist/src/providers/adapters/openrouter.d.ts +14 -0
  97. package/dist/src/providers/adapters/openrouter.d.ts.map +1 -1
  98. package/dist/src/providers/adapters/openrouter.js +53 -1
  99. package/dist/src/providers/adapters/openrouter.js.map +1 -1
  100. package/dist/src/providers/resilient-fetch.d.ts +2 -0
  101. package/dist/src/providers/resilient-fetch.d.ts.map +1 -1
  102. package/dist/src/providers/resilient-fetch.js +27 -3
  103. package/dist/src/providers/resilient-fetch.js.map +1 -1
  104. package/dist/src/providers/types.d.ts +11 -0
  105. package/dist/src/providers/types.d.ts.map +1 -1
  106. package/dist/src/providers/types.js.map +1 -1
  107. package/dist/src/tools/bash.d.ts +2 -2
  108. package/dist/src/tools/file.d.ts +4 -4
  109. package/dist/src/tui/app.d.ts.map +1 -1
  110. package/dist/src/tui/app.js +75 -4
  111. package/dist/src/tui/app.js.map +1 -1
  112. package/dist/src/tui/components/SwarmStatusPanel.d.ts +27 -0
  113. package/dist/src/tui/components/SwarmStatusPanel.d.ts.map +1 -0
  114. package/dist/src/tui/components/SwarmStatusPanel.js +108 -0
  115. package/dist/src/tui/components/SwarmStatusPanel.js.map +1 -0
  116. package/dist/src/types.d.ts +3 -1
  117. package/dist/src/types.d.ts.map +1 -1
  118. package/package.json +1 -1
@@ -0,0 +1,1114 @@
1
+ /**
2
+ * Swarm Orchestrator V2
3
+ *
4
+ * The main orchestration loop that ties together:
5
+ * - SmartDecomposer for task breakdown
6
+ * - SwarmTaskQueue for wave-based scheduling
7
+ * - SwarmWorkerPool for concurrent worker dispatch
8
+ * - SwarmQualityGate for output validation
9
+ * - ResultSynthesizer for merging outputs
10
+ *
11
+ * V2 additions:
12
+ * - Planning phase with acceptance criteria
13
+ * - Post-wave review with fix-up task generation
14
+ * - Integration verification with bash commands
15
+ * - Model health tracking and failover
16
+ * - State persistence and resume
17
+ * - Orchestrator decision logging
18
+ */
19
+ import { createSmartDecomposer, parseDecompositionResponse } from '../smart-decomposer.js';
20
+ import { createResultSynthesizer } from '../result-synthesizer.js';
21
+ import { taskResultToAgentOutput, DEFAULT_SWARM_CONFIG, SUBTASK_TO_CAPABILITY } from './types.js';
22
+ import { createSwarmTaskQueue } from './task-queue.js';
23
+ import { createSwarmBudgetPool } from './swarm-budget.js';
24
+ import { createSwarmWorkerPool } from './worker-pool.js';
25
+ import { evaluateWorkerOutput } from './swarm-quality-gate.js';
26
+ import { ModelHealthTracker, selectAlternativeModel } from './model-selector.js';
27
+ import { SwarmStateStore } from './swarm-state-store.js';
28
+ // ─── Orchestrator ──────────────────────────────────────────────────────────
29
+ export class SwarmOrchestrator {
30
+ config;
31
+ provider;
32
+ blackboard;
33
+ taskQueue;
34
+ budgetPool;
35
+ workerPool;
36
+ _decomposer;
37
+ synthesizer;
38
+ listeners = [];
39
+ errors = [];
40
+ cancelled = false;
41
+ // M5: Explicit phase tracking for TUI status
42
+ currentPhase = 'decomposing';
43
+ // Stats tracking
44
+ totalTokens = 0;
45
+ totalCost = 0;
46
+ qualityRejections = 0;
47
+ retries = 0;
48
+ startTime = 0;
49
+ modelUsage = new Map();
50
+ // V2: Planning, review, verification, health, persistence
51
+ plan;
52
+ waveReviews = [];
53
+ verificationResult;
54
+ orchestratorDecisions = [];
55
+ healthTracker;
56
+ stateStore;
57
+ spawnAgentFn;
58
+ // Circuit breaker: pause all dispatch after too many 429s
59
+ recentRateLimits = [];
60
+ circuitBreakerUntil = 0;
61
+ static CIRCUIT_BREAKER_WINDOW_MS = 30_000;
62
+ static CIRCUIT_BREAKER_THRESHOLD = 3;
63
+ static CIRCUIT_BREAKER_PAUSE_MS = 15_000;
64
+ constructor(config, provider, agentRegistry, spawnAgentFn, blackboard) {
65
+ this.config = { ...DEFAULT_SWARM_CONFIG, ...config };
66
+ this.provider = provider;
67
+ this.blackboard = blackboard;
68
+ this.spawnAgentFn = spawnAgentFn;
69
+ this.healthTracker = new ModelHealthTracker();
70
+ this.taskQueue = createSwarmTaskQueue();
71
+ this.budgetPool = createSwarmBudgetPool(this.config);
72
+ this.workerPool = createSwarmWorkerPool(this.config, agentRegistry, spawnAgentFn, this.budgetPool);
73
+ // Initialize state store if persistence enabled
74
+ if (this.config.enablePersistence) {
75
+ this.stateStore = new SwarmStateStore(this.config.stateDir ?? '.agent/swarm-state', this.config.resumeSessionId);
76
+ }
77
+ // C1: Build LLM decompose function with explicit JSON schema
78
+ const llmDecompose = async (task, _context) => {
79
+ const systemPrompt = `You are a task decomposition expert. Break down the given task into well-defined subtasks with clear dependencies.
80
+
81
+ Respond with valid JSON matching this exact schema:
82
+ {
83
+ "subtasks": [
84
+ {
85
+ "description": "Clear description of what this subtask does",
86
+ "type": "implement" | "research" | "analysis" | "design" | "test" | "refactor" | "review" | "document" | "integrate" | "deploy" | "merge",
87
+ "complexity": 1-10,
88
+ "dependencies": ["description of dependency task or index like '0'"],
89
+ "parallelizable": true | false,
90
+ "relevantFiles": ["src/path/to/file.ts"]
91
+ }
92
+ ],
93
+ "strategy": "sequential" | "parallel" | "hierarchical" | "adaptive" | "pipeline",
94
+ "reasoning": "Brief explanation of why this decomposition was chosen"
95
+ }
96
+
97
+ Rules:
98
+ - Each subtask must have a clear, actionable description
99
+ - Dependencies reference other subtask descriptions or zero-based indices
100
+ - Mark subtasks as parallelizable: true if they don't depend on each other
101
+ - Complexity 1-3: simple, 4-6: moderate, 7-10: complex
102
+ - Return at least 2 subtasks for non-trivial tasks`;
103
+ const response = await this.provider.chat([
104
+ { role: 'system', content: systemPrompt },
105
+ { role: 'user', content: task },
106
+ ], {
107
+ model: this.config.orchestratorModel,
108
+ maxTokens: 4000,
109
+ temperature: 0.3,
110
+ });
111
+ // Use parseDecompositionResponse which handles markdown code blocks and edge cases
112
+ return parseDecompositionResponse(response.content);
113
+ };
114
+ // Configure decomposer for swarm use
115
+ const decomposer = createSmartDecomposer({
116
+ useLLM: true,
117
+ maxSubtasks: 30,
118
+ detectConflicts: true,
119
+ llmProvider: llmDecompose,
120
+ });
121
+ this._decomposer = decomposer;
122
+ this.synthesizer = createResultSynthesizer();
123
+ }
124
+ /**
125
+ * Get the swarm budget pool (used by parent agent to override its own pool).
126
+ */
127
+ getBudgetPool() {
128
+ return this.budgetPool;
129
+ }
130
+ /**
131
+ * Subscribe to swarm events.
132
+ */
133
+ subscribe(listener) {
134
+ this.listeners.push(listener);
135
+ return () => {
136
+ const idx = this.listeners.indexOf(listener);
137
+ if (idx >= 0)
138
+ this.listeners.splice(idx, 1);
139
+ };
140
+ }
141
+ /**
142
+ * Emit a swarm event to all listeners.
143
+ */
144
+ emit(event) {
145
+ for (const listener of this.listeners) {
146
+ try {
147
+ listener(event);
148
+ }
149
+ catch {
150
+ // Don't let listener errors break the orchestrator
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Execute the full swarm pipeline for a task.
156
+ *
157
+ * V2 pipeline:
158
+ * 1. Check for resume
159
+ * 2. Decompose
160
+ * 3. Plan (acceptance criteria + verification plan)
161
+ * 4. Schedule into waves
162
+ * 5. Execute waves with review
163
+ * 6. Verify integration
164
+ * 7. Fix-up loop if verification fails
165
+ * 8. Synthesize
166
+ * 9. Checkpoint (final)
167
+ */
168
+ async execute(task) {
169
+ this.startTime = Date.now();
170
+ try {
171
+ // V2: Check for resume
172
+ if (this.config.resumeSessionId && this.stateStore) {
173
+ return await this.resumeExecution(task);
174
+ }
175
+ // Phase 1: Decompose
176
+ this.currentPhase = 'decomposing';
177
+ const decomposition = await this.decompose(task);
178
+ if (!decomposition) {
179
+ this.currentPhase = 'failed';
180
+ return this.buildErrorResult('Decomposition failed — task may be too simple for swarm mode');
181
+ }
182
+ // Phase 2: Schedule into waves
183
+ this.currentPhase = 'scheduling';
184
+ this.taskQueue.loadFromDecomposition(decomposition, this.config);
185
+ const stats = this.taskQueue.getStats();
186
+ // V2: Phase 2.5: Plan execution (acceptance criteria)
187
+ if (this.config.enablePlanning) {
188
+ this.currentPhase = 'planning';
189
+ await this.planExecution(task, decomposition);
190
+ }
191
+ this.emit({
192
+ type: 'swarm.start',
193
+ taskCount: stats.total,
194
+ waveCount: this.taskQueue.getTotalWaves(),
195
+ config: {
196
+ maxConcurrency: this.config.maxConcurrency,
197
+ totalBudget: this.config.totalBudget,
198
+ maxCost: this.config.maxCost,
199
+ },
200
+ });
201
+ // Emit tasks AFTER swarm.start so the bridge has already initialized
202
+ // (swarm.start clears tasks/edges, so loading before it would lose them)
203
+ this.emit({
204
+ type: 'swarm.tasks.loaded',
205
+ tasks: this.taskQueue.getAllTasks(),
206
+ });
207
+ // Phase 3: Execute waves (V2: with review after each wave)
208
+ this.currentPhase = 'executing';
209
+ await this.executeWaves();
210
+ // V2: Phase 3.5: Verify integration
211
+ if (this.config.enableVerification && this.plan?.integrationTestPlan) {
212
+ this.currentPhase = 'verifying';
213
+ const verification = await this.verifyIntegration(this.plan.integrationTestPlan);
214
+ if (!verification.passed) {
215
+ await this.handleVerificationFailure(verification, task);
216
+ }
217
+ }
218
+ // Phase 4: Synthesize results
219
+ this.currentPhase = 'synthesizing';
220
+ const synthesisResult = await this.synthesize();
221
+ this.currentPhase = 'completed';
222
+ const executionStats = this.buildStats();
223
+ // V2: Final checkpoint
224
+ this.checkpoint('final');
225
+ this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors });
226
+ return {
227
+ success: executionStats.completedTasks > 0,
228
+ synthesisResult: synthesisResult ?? undefined,
229
+ summary: this.buildSummary(executionStats),
230
+ tasks: this.taskQueue.getAllTasks(),
231
+ stats: executionStats,
232
+ errors: this.errors,
233
+ };
234
+ }
235
+ catch (error) {
236
+ this.currentPhase = 'failed';
237
+ const message = error.message;
238
+ this.errors.push({
239
+ phase: 'execution',
240
+ message,
241
+ recovered: false,
242
+ });
243
+ this.emit({ type: 'swarm.error', error: message, phase: 'execution' });
244
+ return this.buildErrorResult(message);
245
+ }
246
+ finally {
247
+ this.workerPool.cleanup();
248
+ }
249
+ }
250
+ /**
251
+ * Phase 1: Decompose the task into subtasks.
252
+ */
253
+ async decompose(task) {
254
+ try {
255
+ const result = await this._decomposer.decompose(task);
256
+ if (result.subtasks.length < 2) {
257
+ // Too simple for swarm mode
258
+ return null;
259
+ }
260
+ return result;
261
+ }
262
+ catch (error) {
263
+ this.errors.push({
264
+ phase: 'decomposition',
265
+ message: error.message,
266
+ recovered: false,
267
+ });
268
+ this.emit({ type: 'swarm.error', error: error.message, phase: 'decomposition' });
269
+ return null;
270
+ }
271
+ }
272
+ // ─── V2: Planning Phase ───────────────────────────────────────────────
273
+ /**
274
+ * Create acceptance criteria and integration test plan.
275
+ * Graceful: if planning fails, continues without criteria.
276
+ */
277
+ async planExecution(task, decomposition) {
278
+ try {
279
+ // V3: Manager role handles planning
280
+ const plannerModel = this.config.hierarchy?.manager?.model
281
+ ?? this.config.plannerModel ?? this.config.orchestratorModel;
282
+ this.emit({ type: 'swarm.role.action', role: 'manager', action: 'plan', model: plannerModel });
283
+ this.logDecision('planning', `Creating acceptance criteria (manager: ${plannerModel})`, `Task has ${decomposition.subtasks.length} subtasks, planning to ensure quality`);
284
+ const taskList = decomposition.subtasks
285
+ .map(s => `- [${s.id}] (${s.type}): ${s.description}`)
286
+ .join('\n');
287
+ const response = await this.provider.chat([
288
+ {
289
+ role: 'system',
290
+ content: `You are a project quality planner. Given a task and its decomposition into subtasks, create:
291
+ 1. Acceptance criteria for each subtask (what "done" looks like)
292
+ 2. An integration test plan (bash commands to verify the combined result works)
293
+
294
+ Respond with valid JSON:
295
+ {
296
+ "acceptanceCriteria": [
297
+ { "taskId": "st-0", "criteria": ["criterion 1", "criterion 2"] }
298
+ ],
299
+ "integrationTestPlan": {
300
+ "description": "What this test plan verifies",
301
+ "steps": [
302
+ { "description": "Check if files exist", "command": "ls src/parser.js", "expectedResult": "file listed", "required": true }
303
+ ],
304
+ "successCriteria": "All required steps pass"
305
+ },
306
+ "reasoning": "Why this plan was chosen"
307
+ }`,
308
+ },
309
+ {
310
+ role: 'user',
311
+ content: `Task: ${task}\n\nSubtasks:\n${taskList}`,
312
+ },
313
+ ], {
314
+ model: plannerModel,
315
+ maxTokens: 3000,
316
+ temperature: 0.3,
317
+ });
318
+ const parsed = this.parseJSON(response.content);
319
+ if (parsed) {
320
+ this.plan = {
321
+ acceptanceCriteria: parsed.acceptanceCriteria ?? [],
322
+ integrationTestPlan: parsed.integrationTestPlan,
323
+ reasoning: parsed.reasoning ?? '',
324
+ };
325
+ this.emit({
326
+ type: 'swarm.plan.complete',
327
+ criteriaCount: this.plan.acceptanceCriteria.length,
328
+ hasIntegrationPlan: !!this.plan.integrationTestPlan,
329
+ });
330
+ }
331
+ }
332
+ catch (error) {
333
+ // Graceful fallback: continue without plan
334
+ this.errors.push({
335
+ phase: 'planning',
336
+ message: `Planning failed (non-fatal): ${error.message}`,
337
+ recovered: true,
338
+ });
339
+ }
340
+ }
341
+ // ─── V2: Wave Review ──────────────────────────────────────────────────
342
+ /**
343
+ * Review completed wave outputs against acceptance criteria.
344
+ * May spawn fix-up tasks for issues found.
345
+ */
346
+ async reviewWave(waveIndex) {
347
+ if (!this.config.enableWaveReview)
348
+ return null;
349
+ try {
350
+ // V3: Manager role handles wave review
351
+ const managerModel = this.config.hierarchy?.manager?.model
352
+ ?? this.config.plannerModel ?? this.config.orchestratorModel;
353
+ const managerPersona = this.config.hierarchy?.manager?.persona;
354
+ this.emit({ type: 'swarm.role.action', role: 'manager', action: 'review', model: managerModel, wave: waveIndex + 1 });
355
+ this.emit({ type: 'swarm.review.start', wave: waveIndex + 1 });
356
+ this.logDecision('review', `Reviewing wave ${waveIndex + 1} outputs (manager: ${managerModel})`, 'Checking task outputs against acceptance criteria');
357
+ const completedTasks = this.taskQueue.getAllTasks()
358
+ .filter(t => t.status === 'completed' && t.wave === waveIndex);
359
+ if (completedTasks.length === 0) {
360
+ return { wave: waveIndex, assessment: 'good', taskAssessments: [], fixupTasks: [] };
361
+ }
362
+ // Build review prompt
363
+ const taskSummaries = completedTasks.map(t => {
364
+ const criteria = this.plan?.acceptanceCriteria.find(c => c.taskId === t.id);
365
+ return `Task ${t.id}: ${t.description}
366
+ Output: ${t.result?.output?.slice(0, 500) ?? 'No output'}
367
+ Acceptance criteria: ${criteria?.criteria.join('; ') ?? 'None set'}`;
368
+ }).join('\n\n');
369
+ const reviewModel = managerModel;
370
+ const reviewSystemPrompt = managerPersona
371
+ ? `${managerPersona}\n\nYou are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`
372
+ : `You are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`;
373
+ const response = await this.provider.chat([
374
+ {
375
+ role: 'system',
376
+ content: `${reviewSystemPrompt}
377
+ {
378
+ "assessment": "good" | "needs-fixes" | "critical-issues",
379
+ "taskAssessments": [
380
+ { "taskId": "st-0", "passed": true, "feedback": "optional feedback" }
381
+ ],
382
+ "fixupInstructions": [
383
+ { "fixesTaskId": "st-0", "description": "What to fix", "instructions": "Specific fix instructions" }
384
+ ]
385
+ }`,
386
+ },
387
+ { role: 'user', content: `Review these wave ${waveIndex + 1} outputs:\n\n${taskSummaries}` },
388
+ ], { model: reviewModel, maxTokens: 2000, temperature: 0.3 });
389
+ const parsed = this.parseJSON(response.content);
390
+ if (!parsed)
391
+ return null;
392
+ // Create fix-up tasks
393
+ const fixupTasks = [];
394
+ if (parsed.fixupInstructions) {
395
+ for (const fix of parsed.fixupInstructions) {
396
+ const fixupId = `fixup-${fix.fixesTaskId}-${Date.now()}`;
397
+ const originalTask = this.taskQueue.getTask(fix.fixesTaskId);
398
+ const fixupTask = {
399
+ id: fixupId,
400
+ description: fix.description,
401
+ type: originalTask?.type ?? 'implement',
402
+ dependencies: [fix.fixesTaskId],
403
+ status: 'ready',
404
+ complexity: 3,
405
+ wave: waveIndex,
406
+ attempts: 0,
407
+ fixesTaskId: fix.fixesTaskId,
408
+ fixInstructions: fix.instructions,
409
+ };
410
+ fixupTasks.push(fixupTask);
411
+ this.emit({ type: 'swarm.fixup.spawned', taskId: fixupId, fixesTaskId: fix.fixesTaskId, description: fix.description });
412
+ }
413
+ if (fixupTasks.length > 0) {
414
+ this.taskQueue.addFixupTasks(fixupTasks);
415
+ }
416
+ }
417
+ const result = {
418
+ wave: waveIndex,
419
+ assessment: parsed.assessment ?? 'good',
420
+ taskAssessments: parsed.taskAssessments ?? [],
421
+ fixupTasks,
422
+ };
423
+ this.waveReviews.push(result);
424
+ this.emit({
425
+ type: 'swarm.review.complete',
426
+ wave: waveIndex + 1,
427
+ assessment: result.assessment,
428
+ fixupCount: fixupTasks.length,
429
+ });
430
+ return result;
431
+ }
432
+ catch (error) {
433
+ // Graceful: continue without review
434
+ this.errors.push({
435
+ phase: 'review',
436
+ message: `Wave review failed (non-fatal): ${error.message}`,
437
+ recovered: true,
438
+ });
439
+ return null;
440
+ }
441
+ }
442
+ // ─── V2: Verification Phase ───────────────────────────────────────────
443
+ /**
444
+ * Run integration verification steps.
445
+ */
446
+ async verifyIntegration(testPlan) {
447
+ // V3: Judge role handles verification
448
+ const verifyModel = this.config.hierarchy?.judge?.model
449
+ ?? this.config.qualityGateModel ?? this.config.orchestratorModel;
450
+ this.emit({ type: 'swarm.role.action', role: 'judge', action: 'verify', model: verifyModel });
451
+ this.emit({ type: 'swarm.verify.start', stepCount: testPlan.steps.length });
452
+ this.logDecision('verification', `Running ${testPlan.steps.length} verification steps (judge: ${verifyModel})`, testPlan.description);
453
+ const stepResults = [];
454
+ let allRequiredPassed = true;
455
+ for (let i = 0; i < testPlan.steps.length; i++) {
456
+ const step = testPlan.steps[i];
457
+ try {
458
+ // Use spawnAgent to execute verification command safely
459
+ const verifierName = `swarm-verifier-${i}`;
460
+ const result = await this.spawnAgentFn(verifierName, `Run this command and report the result: ${step.command}\nExpected: ${step.expectedResult ?? 'success'}`);
461
+ const passed = result.success;
462
+ stepResults.push({ step, passed, output: result.output.slice(0, 500) });
463
+ if (!passed && step.required) {
464
+ allRequiredPassed = false;
465
+ }
466
+ this.emit({ type: 'swarm.verify.step', stepIndex: i, description: step.description, passed });
467
+ }
468
+ catch (error) {
469
+ const output = `Error: ${error.message}`;
470
+ stepResults.push({ step, passed: false, output });
471
+ if (step.required)
472
+ allRequiredPassed = false;
473
+ this.emit({ type: 'swarm.verify.step', stepIndex: i, description: step.description, passed: false });
474
+ }
475
+ }
476
+ const verificationResult = {
477
+ passed: allRequiredPassed,
478
+ stepResults,
479
+ summary: allRequiredPassed
480
+ ? `All ${stepResults.filter(r => r.passed).length}/${stepResults.length} steps passed`
481
+ : `${stepResults.filter(r => !r.passed).length}/${stepResults.length} steps failed`,
482
+ };
483
+ this.verificationResult = verificationResult;
484
+ this.emit({ type: 'swarm.verify.complete', result: verificationResult });
485
+ return verificationResult;
486
+ }
487
+ /**
488
+ * Handle verification failure: create fix-up tasks and re-verify.
489
+ */
490
+ async handleVerificationFailure(verification, task) {
491
+ const maxRetries = this.config.maxVerificationRetries ?? 2;
492
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
493
+ this.logDecision('verification', `Verification failed, fix-up attempt ${attempt + 1}/${maxRetries}`, `${verification.stepResults.filter(r => !r.passed).length} steps failed`);
494
+ // Ask orchestrator what to fix
495
+ try {
496
+ const failedSteps = verification.stepResults
497
+ .filter(r => !r.passed)
498
+ .map(r => `- ${r.step.description}: ${r.output}`)
499
+ .join('\n');
500
+ const response = await this.provider.chat([
501
+ {
502
+ role: 'system',
503
+ content: `Verification failed. Analyze the failures and create fix-up tasks.
504
+ Respond with JSON: { "fixups": [{ "description": "what to fix", "type": "implement" }] }`,
505
+ },
506
+ { role: 'user', content: `Original task: ${task}\n\nFailed verifications:\n${failedSteps}` },
507
+ ], { model: this.config.plannerModel ?? this.config.orchestratorModel, maxTokens: 1500, temperature: 0.3 });
508
+ const parsed = this.parseJSON(response.content);
509
+ if (parsed?.fixups && parsed.fixups.length > 0) {
510
+ const fixupTasks = parsed.fixups.map((f, i) => ({
511
+ id: `verify-fix-${attempt}-${i}-${Date.now()}`,
512
+ description: f.description,
513
+ type: (f.type ?? 'implement'),
514
+ dependencies: [],
515
+ status: 'ready',
516
+ complexity: 4,
517
+ wave: this.taskQueue.getCurrentWave(),
518
+ attempts: 0,
519
+ fixesTaskId: 'verification',
520
+ fixInstructions: f.description,
521
+ }));
522
+ this.taskQueue.addFixupTasks(fixupTasks);
523
+ // Execute fix-up wave
524
+ this.currentPhase = 'executing';
525
+ await this.executeWave(fixupTasks);
526
+ // Re-verify
527
+ this.currentPhase = 'verifying';
528
+ verification = await this.verifyIntegration(this.plan.integrationTestPlan);
529
+ if (verification.passed)
530
+ return;
531
+ }
532
+ }
533
+ catch {
534
+ // Continue to next attempt
535
+ }
536
+ }
537
+ }
538
+ // ─── V2: Resume ───────────────────────────────────────────────────────
539
+ /**
540
+ * Resume execution from a saved checkpoint.
541
+ */
542
+ async resumeExecution(task) {
543
+ const checkpoint = SwarmStateStore.loadLatest(this.config.stateDir ?? '.agent/swarm-state', this.config.resumeSessionId);
544
+ if (!checkpoint) {
545
+ this.logDecision('resume', 'No checkpoint found, starting fresh', `Session: ${this.config.resumeSessionId}`);
546
+ // Clear resume flag and execute normally
547
+ this.config.resumeSessionId = undefined;
548
+ return this.execute(task);
549
+ }
550
+ this.logDecision('resume', `Resuming from wave ${checkpoint.currentWave}`, `Session: ${checkpoint.sessionId}`);
551
+ this.emit({ type: 'swarm.state.resume', sessionId: checkpoint.sessionId, fromWave: checkpoint.currentWave });
552
+ // Restore state
553
+ if (checkpoint.plan)
554
+ this.plan = checkpoint.plan;
555
+ if (checkpoint.modelHealth.length > 0)
556
+ this.healthTracker.restore(checkpoint.modelHealth);
557
+ this.orchestratorDecisions = checkpoint.decisions ?? [];
558
+ this.errors = checkpoint.errors ?? [];
559
+ this.totalTokens = checkpoint.stats.totalTokens;
560
+ this.totalCost = checkpoint.stats.totalCost;
561
+ this.qualityRejections = checkpoint.stats.qualityRejections;
562
+ this.retries = checkpoint.stats.retries;
563
+ // Restore task queue
564
+ this.taskQueue.restoreFromCheckpoint({
565
+ taskStates: checkpoint.taskStates,
566
+ waves: checkpoint.waves,
567
+ currentWave: checkpoint.currentWave,
568
+ });
569
+ // Continue from where we left off
570
+ this.currentPhase = 'executing';
571
+ await this.executeWaves();
572
+ // Continue with verification and synthesis as normal
573
+ if (this.config.enableVerification && this.plan?.integrationTestPlan) {
574
+ this.currentPhase = 'verifying';
575
+ const verification = await this.verifyIntegration(this.plan.integrationTestPlan);
576
+ if (!verification.passed) {
577
+ await this.handleVerificationFailure(verification, task);
578
+ }
579
+ }
580
+ this.currentPhase = 'synthesizing';
581
+ const synthesisResult = await this.synthesize();
582
+ this.currentPhase = 'completed';
583
+ const executionStats = this.buildStats();
584
+ this.checkpoint('final');
585
+ this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors });
586
+ return {
587
+ success: executionStats.completedTasks > 0,
588
+ synthesisResult: synthesisResult ?? undefined,
589
+ summary: this.buildSummary(executionStats),
590
+ tasks: this.taskQueue.getAllTasks(),
591
+ stats: executionStats,
592
+ errors: this.errors,
593
+ };
594
+ }
595
+ // ─── Wave Execution ───────────────────────────────────────────────────
596
+ /**
597
+ * Execute all waves in sequence, with review after each.
598
+ */
599
+ async executeWaves() {
600
+ let waveIndex = this.taskQueue.getCurrentWave();
601
+ const totalWaves = this.taskQueue.getTotalWaves();
602
+ while (waveIndex < totalWaves && !this.cancelled) {
603
+ const readyTasks = this.taskQueue.getReadyTasks();
604
+ const queueStats = this.taskQueue.getStats();
605
+ this.emit({
606
+ type: 'swarm.wave.start',
607
+ wave: waveIndex + 1,
608
+ totalWaves,
609
+ taskCount: readyTasks.length,
610
+ });
611
+ // Dispatch tasks up to concurrency limit
612
+ await this.executeWave(readyTasks);
613
+ // Wave complete stats
614
+ const afterStats = this.taskQueue.getStats();
615
+ const waveCompleted = afterStats.completed - (queueStats.completed);
616
+ const waveFailed = afterStats.failed - (queueStats.failed);
617
+ const waveSkipped = afterStats.skipped - (queueStats.skipped);
618
+ this.emit({
619
+ type: 'swarm.wave.complete',
620
+ wave: waveIndex + 1,
621
+ totalWaves,
622
+ completed: waveCompleted,
623
+ failed: waveFailed,
624
+ skipped: waveSkipped,
625
+ });
626
+ // V2: Review wave outputs
627
+ const review = await this.reviewWave(waveIndex);
628
+ if (review && review.fixupTasks.length > 0) {
629
+ // Execute fix-up tasks immediately
630
+ await this.executeWave(review.fixupTasks);
631
+ }
632
+ // V2: Checkpoint after each wave
633
+ this.checkpoint(`wave-${waveIndex}`);
634
+ // Advance to next wave
635
+ if (!this.taskQueue.advanceWave())
636
+ break;
637
+ waveIndex++;
638
+ }
639
+ }
640
+ /**
641
+ * Execute a single wave's tasks with concurrency control.
642
+ */
643
+ async executeWave(tasks) {
644
+ // Dispatch initial batch with stagger to avoid rate limit storms
645
+ let taskIndex = 0;
646
+ while (taskIndex < tasks.length && this.workerPool.availableSlots > 0 && !this.cancelled) {
647
+ // Circuit breaker: wait if tripped
648
+ if (this.isCircuitBreakerActive()) {
649
+ const waitMs = this.circuitBreakerUntil - Date.now();
650
+ if (waitMs > 0)
651
+ await new Promise(resolve => setTimeout(resolve, waitMs));
652
+ continue; // Re-check after wait
653
+ }
654
+ const task = tasks[taskIndex];
655
+ await this.dispatchTask(task);
656
+ taskIndex++;
657
+ // Stagger dispatches to avoid rate limit storms
658
+ if (taskIndex < tasks.length && this.workerPool.availableSlots > 0) {
659
+ await new Promise(resolve => setTimeout(resolve, this.config.dispatchStaggerMs ?? 500));
660
+ }
661
+ }
662
+ // Process completions and dispatch more tasks as slots open
663
+ while (this.workerPool.activeCount > 0 && !this.cancelled) {
664
+ const completed = await this.workerPool.waitForAny();
665
+ if (!completed)
666
+ break;
667
+ // H2: Use per-task startedAt for accurate duration (not orchestrator startTime)
668
+ await this.handleTaskCompletion(completed.taskId, completed.result, completed.startedAt);
669
+ // Emit budget update
670
+ this.emitBudgetUpdate();
671
+ // Emit status update
672
+ this.emitStatusUpdate();
673
+ // Dispatch more tasks if slots available and tasks remain
674
+ while (taskIndex < tasks.length && this.workerPool.availableSlots > 0 && !this.cancelled) {
675
+ const task = tasks[taskIndex];
676
+ if (task.status === 'ready') {
677
+ await this.dispatchTask(task);
678
+ // Stagger dispatches to avoid rate limit storms
679
+ if (taskIndex + 1 < tasks.length && this.workerPool.availableSlots > 0) {
680
+ await new Promise(resolve => setTimeout(resolve, this.config.dispatchStaggerMs ?? 500));
681
+ }
682
+ }
683
+ taskIndex++;
684
+ }
685
+ // Also check for cross-wave ready tasks to fill slots (skip if circuit breaker active)
686
+ if (this.workerPool.availableSlots > 0 && !this.isCircuitBreakerActive()) {
687
+ const moreReady = this.taskQueue.getAllReadyTasks()
688
+ .filter(t => !this.workerPool.getActiveWorkerStatus().some(w => w.taskId === t.id));
689
+ for (let i = 0; i < moreReady.length; i++) {
690
+ if (this.workerPool.availableSlots <= 0)
691
+ break;
692
+ await this.dispatchTask(moreReady[i]);
693
+ // Stagger dispatches to avoid rate limit storms
694
+ if (i + 1 < moreReady.length && this.workerPool.availableSlots > 0) {
695
+ await new Promise(resolve => setTimeout(resolve, this.config.dispatchStaggerMs ?? 500));
696
+ }
697
+ }
698
+ }
699
+ }
700
+ }
701
+ /**
702
+ * Dispatch a single task to a worker.
703
+ */
704
+ async dispatchTask(task) {
705
+ const worker = this.workerPool.selectWorker(task);
706
+ if (!worker) {
707
+ // M2: Emit error and mark task failed instead of silently returning
708
+ this.taskQueue.markFailed(task.id, 0);
709
+ this.emit({
710
+ type: 'swarm.task.failed',
711
+ taskId: task.id,
712
+ error: `No worker available for task type: ${task.type}`,
713
+ attempt: 0,
714
+ maxAttempts: 0,
715
+ willRetry: false,
716
+ });
717
+ return;
718
+ }
719
+ try {
720
+ this.taskQueue.markDispatched(task.id, worker.model);
721
+ await this.workerPool.dispatch(task);
722
+ this.emit({
723
+ type: 'swarm.task.dispatched',
724
+ taskId: task.id,
725
+ description: task.description,
726
+ model: worker.model,
727
+ workerName: worker.name,
728
+ });
729
+ }
730
+ catch (error) {
731
+ this.errors.push({
732
+ taskId: task.id,
733
+ phase: 'dispatch',
734
+ message: error.message,
735
+ recovered: false,
736
+ });
737
+ this.emit({
738
+ type: 'swarm.task.failed',
739
+ taskId: task.id,
740
+ error: error.message,
741
+ attempt: task.attempts,
742
+ maxAttempts: 1 + this.config.workerRetries,
743
+ willRetry: false,
744
+ });
745
+ this.taskQueue.markFailed(task.id, 0);
746
+ }
747
+ }
748
+ /**
749
+ * Handle a completed task: quality gate, bookkeeping, retry logic, model health, failover.
750
+ */
751
+ async handleTaskCompletion(taskId, spawnResult, startedAt) {
752
+ const task = this.taskQueue.getTask(taskId);
753
+ if (!task)
754
+ return;
755
+ const durationMs = Date.now() - startedAt;
756
+ const taskResult = this.workerPool.toTaskResult(spawnResult, task, durationMs);
757
+ // Track model usage
758
+ const model = task.assignedModel ?? 'unknown';
759
+ const usage = this.modelUsage.get(model) ?? { tasks: 0, tokens: 0, cost: 0 };
760
+ usage.tasks++;
761
+ usage.tokens += taskResult.tokensUsed;
762
+ usage.cost += taskResult.costUsed;
763
+ this.modelUsage.set(model, usage);
764
+ this.totalTokens += taskResult.tokensUsed;
765
+ this.totalCost += taskResult.costUsed;
766
+ if (!spawnResult.success) {
767
+ // V2: Record model health
768
+ const errorMsg = spawnResult.output.toLowerCase();
769
+ const is429 = errorMsg.includes('429') || errorMsg.includes('rate');
770
+ const is402 = errorMsg.includes('402') || errorMsg.includes('spend limit');
771
+ const errorType = is429 ? '429' : is402 ? '402' : 'error';
772
+ this.healthTracker.recordFailure(model, errorType);
773
+ this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
774
+ // Feed circuit breaker
775
+ if (is429 || is402) {
776
+ this.recordRateLimit();
777
+ }
778
+ // V2: Model failover on rate limits
779
+ if ((is429 || is402) && this.config.enableModelFailover) {
780
+ const capability = SUBTASK_TO_CAPABILITY[task.type] ?? 'code';
781
+ const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
782
+ if (alternative) {
783
+ this.emit({
784
+ type: 'swarm.model.failover',
785
+ taskId,
786
+ fromModel: model,
787
+ toModel: alternative.model,
788
+ reason: errorType,
789
+ });
790
+ task.assignedModel = alternative.model;
791
+ this.logDecision('failover', `Switched ${taskId} from ${model} to ${alternative.model}`, `${errorType} error`);
792
+ }
793
+ }
794
+ // Worker failed — use higher retry limit for rate limit errors
795
+ const retryLimit = (is429 || is402)
796
+ ? (this.config.rateLimitRetries ?? 3)
797
+ : this.config.workerRetries;
798
+ const canRetry = this.taskQueue.markFailed(taskId, retryLimit);
799
+ if (canRetry) {
800
+ this.retries++;
801
+ // Non-blocking cooldown: set retryAfter timestamp instead of blocking
802
+ if (is429 || is402) {
803
+ const baseDelay = this.config.retryBaseDelayMs ?? 5000;
804
+ const cooldownMs = Math.min(baseDelay * Math.pow(2, task.attempts - 1), 30000);
805
+ this.taskQueue.setRetryAfter(taskId, cooldownMs);
806
+ }
807
+ }
808
+ this.emit({
809
+ type: 'swarm.task.failed',
810
+ taskId,
811
+ error: spawnResult.output.slice(0, 200),
812
+ attempt: task.attempts,
813
+ maxAttempts: 1 + this.config.workerRetries,
814
+ willRetry: canRetry,
815
+ });
816
+ return;
817
+ }
818
+ // V2: Record model health on success
819
+ this.healthTracker.recordSuccess(model, durationMs);
820
+ // Run quality gate if enabled — skip under API pressure or on retried tasks
821
+ const recentRLCount = this.recentRateLimits.filter(t => t > Date.now() - 30_000).length;
822
+ const shouldRunQualityGate = this.config.qualityGates
823
+ && task.attempts <= 1
824
+ && Date.now() >= this.circuitBreakerUntil
825
+ && recentRLCount < 2;
826
+ if (shouldRunQualityGate) {
827
+ // V3: Judge role handles quality gates
828
+ const judgeModel = this.config.hierarchy?.judge?.model
829
+ ?? this.config.qualityGateModel ?? this.config.orchestratorModel;
830
+ const judgeConfig = {
831
+ model: judgeModel,
832
+ persona: this.config.hierarchy?.judge?.persona,
833
+ };
834
+ this.emit({ type: 'swarm.role.action', role: 'judge', action: 'quality-gate', model: judgeModel, taskId });
835
+ const quality = await evaluateWorkerOutput(this.provider, judgeModel, task, taskResult, judgeConfig);
836
+ taskResult.qualityScore = quality.score;
837
+ taskResult.qualityFeedback = quality.feedback;
838
+ if (!quality.passed) {
839
+ this.qualityRejections++;
840
+ const canRetry = this.taskQueue.markFailed(taskId, this.config.workerRetries);
841
+ if (canRetry) {
842
+ this.retries++;
843
+ }
844
+ // M1: Only emit quality.rejected (not duplicate task.failed)
845
+ this.emit({
846
+ type: 'swarm.quality.rejected',
847
+ taskId,
848
+ score: quality.score,
849
+ feedback: quality.feedback,
850
+ });
851
+ return;
852
+ }
853
+ }
854
+ // Task passed — mark completed
855
+ this.taskQueue.markCompleted(taskId, taskResult);
856
+ // H6: Post findings to blackboard with error handling
857
+ if (this.blackboard && taskResult.findings) {
858
+ try {
859
+ for (const finding of taskResult.findings) {
860
+ this.blackboard.post(`swarm-worker-${taskId}`, {
861
+ topic: `swarm.task.${task.type}`,
862
+ content: finding,
863
+ type: 'progress',
864
+ confidence: (taskResult.qualityScore ?? 3) / 5,
865
+ tags: ['swarm', task.type],
866
+ relatedFiles: task.targetFiles,
867
+ });
868
+ }
869
+ }
870
+ catch {
871
+ // Don't crash orchestrator on blackboard failures
872
+ this.errors.push({
873
+ taskId,
874
+ phase: 'execution',
875
+ message: 'Failed to post findings to blackboard',
876
+ recovered: true,
877
+ });
878
+ }
879
+ }
880
+ this.emit({
881
+ type: 'swarm.task.completed',
882
+ taskId,
883
+ success: true,
884
+ tokensUsed: taskResult.tokensUsed,
885
+ costUsed: taskResult.costUsed,
886
+ durationMs: taskResult.durationMs,
887
+ qualityScore: taskResult.qualityScore,
888
+ });
889
+ }
890
+ /**
891
+ * Phase 4: Synthesize all completed task outputs.
892
+ */
893
+ async synthesize() {
894
+ const tasks = this.taskQueue.getAllTasks();
895
+ const outputs = tasks
896
+ .filter(t => t.status === 'completed')
897
+ .map(t => taskResultToAgentOutput(t))
898
+ .filter((o) => o !== null);
899
+ if (outputs.length === 0)
900
+ return null;
901
+ try {
902
+ return await this.synthesizer.synthesize(outputs);
903
+ }
904
+ catch (error) {
905
+ this.errors.push({
906
+ phase: 'synthesis',
907
+ message: error.message,
908
+ recovered: true,
909
+ });
910
+ // Fallback: concatenate outputs
911
+ return this.synthesizer.synthesizeFindings(outputs);
912
+ }
913
+ }
914
+ /**
915
+ * Get live status for TUI.
916
+ */
917
+ // M5: Use explicit phase tracking instead of inferring from queue state
918
+ getStatus() {
919
+ const stats = this.taskQueue.getStats();
920
+ return {
921
+ phase: this.cancelled ? 'failed' : this.currentPhase,
922
+ currentWave: this.taskQueue.getCurrentWave() + 1,
923
+ totalWaves: this.taskQueue.getTotalWaves(),
924
+ activeWorkers: this.workerPool.getActiveWorkerStatus(),
925
+ queue: stats,
926
+ budget: {
927
+ tokensUsed: this.totalTokens,
928
+ tokensTotal: this.config.totalBudget,
929
+ costUsed: this.totalCost,
930
+ costTotal: this.config.maxCost,
931
+ },
932
+ };
933
+ }
934
+ /**
935
+ * Cancel the swarm execution.
936
+ * M6: Wait for active workers before cleanup.
937
+ */
938
+ async cancel() {
939
+ this.cancelled = true;
940
+ this.currentPhase = 'failed';
941
+ await this.workerPool.cancelAll();
942
+ }
943
+ // ─── Circuit Breaker ────────────────────────────────────────────────
944
+ /**
945
+ * Record a rate limit hit and check if the circuit breaker should trip.
946
+ */
947
+ recordRateLimit() {
948
+ const now = Date.now();
949
+ this.recentRateLimits.push(now);
950
+ // Prune entries older than the window
951
+ const cutoff = now - SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS;
952
+ this.recentRateLimits = this.recentRateLimits.filter(t => t > cutoff);
953
+ if (this.recentRateLimits.length >= SwarmOrchestrator.CIRCUIT_BREAKER_THRESHOLD) {
954
+ this.circuitBreakerUntil = now + SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS;
955
+ this.emit({
956
+ type: 'swarm.circuit.open',
957
+ recentCount: this.recentRateLimits.length,
958
+ pauseMs: SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS,
959
+ });
960
+ this.logDecision('circuit-breaker', 'Tripped — pausing all dispatch', `${this.recentRateLimits.length} rate limits in ${SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS / 1000}s window`);
961
+ }
962
+ }
963
+ /**
964
+ * Check if the circuit breaker is currently active.
965
+ * Returns true if dispatch should be paused.
966
+ */
967
+ isCircuitBreakerActive() {
968
+ if (Date.now() < this.circuitBreakerUntil)
969
+ return true;
970
+ if (this.circuitBreakerUntil > 0) {
971
+ // Circuit just closed
972
+ this.circuitBreakerUntil = 0;
973
+ this.emit({ type: 'swarm.circuit.closed' });
974
+ }
975
+ return false;
976
+ }
977
+ // ─── V2: Decision Logging ─────────────────────────────────────────────
978
+ logDecision(phase, decision, reasoning) {
979
+ const entry = {
980
+ timestamp: Date.now(),
981
+ phase,
982
+ decision,
983
+ reasoning,
984
+ };
985
+ this.orchestratorDecisions.push(entry);
986
+ this.emit({ type: 'swarm.orchestrator.decision', decision: entry });
987
+ }
988
+ // ─── V2: Persistence ──────────────────────────────────────────────────
989
+ checkpoint(_label) {
990
+ if (!this.config.enablePersistence || !this.stateStore)
991
+ return;
992
+ try {
993
+ const queueState = this.taskQueue.getCheckpointState();
994
+ this.stateStore.saveCheckpoint({
995
+ sessionId: this.stateStore.id,
996
+ timestamp: Date.now(),
997
+ phase: this.currentPhase,
998
+ plan: this.plan,
999
+ taskStates: queueState.taskStates,
1000
+ waves: queueState.waves,
1001
+ currentWave: queueState.currentWave,
1002
+ stats: {
1003
+ totalTokens: this.totalTokens,
1004
+ totalCost: this.totalCost,
1005
+ qualityRejections: this.qualityRejections,
1006
+ retries: this.retries,
1007
+ },
1008
+ modelHealth: this.healthTracker.getAllRecords(),
1009
+ decisions: this.orchestratorDecisions,
1010
+ errors: this.errors,
1011
+ });
1012
+ this.emit({
1013
+ type: 'swarm.state.checkpoint',
1014
+ sessionId: this.stateStore.id,
1015
+ wave: this.taskQueue.getCurrentWave(),
1016
+ });
1017
+ }
1018
+ catch (error) {
1019
+ this.errors.push({
1020
+ phase: 'persistence',
1021
+ message: `Checkpoint failed (non-fatal): ${error.message}`,
1022
+ recovered: true,
1023
+ });
1024
+ }
1025
+ }
1026
+ // ─── Private Helpers ───────────────────────────────────────────────────
1027
+ emitBudgetUpdate() {
1028
+ this.emit({
1029
+ type: 'swarm.budget.update',
1030
+ tokensUsed: this.totalTokens,
1031
+ tokensTotal: this.config.totalBudget,
1032
+ costUsed: this.totalCost,
1033
+ costTotal: this.config.maxCost,
1034
+ });
1035
+ }
1036
+ emitStatusUpdate() {
1037
+ this.emit({ type: 'swarm.status', status: this.getStatus() });
1038
+ }
1039
+ buildStats() {
1040
+ const queueStats = this.taskQueue.getStats();
1041
+ return {
1042
+ totalTasks: queueStats.total,
1043
+ completedTasks: queueStats.completed,
1044
+ failedTasks: queueStats.failed,
1045
+ skippedTasks: queueStats.skipped,
1046
+ totalWaves: this.taskQueue.getTotalWaves(),
1047
+ totalTokens: this.totalTokens,
1048
+ totalCost: this.totalCost,
1049
+ totalDurationMs: Date.now() - this.startTime,
1050
+ qualityRejections: this.qualityRejections,
1051
+ retries: this.retries,
1052
+ modelUsage: this.modelUsage,
1053
+ };
1054
+ }
1055
+ buildSummary(stats) {
1056
+ const parts = [
1057
+ `Swarm execution complete:`,
1058
+ ` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed, ${stats.failedTasks} failed, ${stats.skippedTasks} skipped`,
1059
+ ` Waves: ${stats.totalWaves}`,
1060
+ ` Tokens: ${(stats.totalTokens / 1000).toFixed(0)}k`,
1061
+ ` Cost: $${stats.totalCost.toFixed(4)}`,
1062
+ ` Duration: ${(stats.totalDurationMs / 1000).toFixed(1)}s`,
1063
+ ];
1064
+ if (stats.qualityRejections > 0) {
1065
+ parts.push(` Quality rejections: ${stats.qualityRejections}`);
1066
+ }
1067
+ if (stats.retries > 0) {
1068
+ parts.push(` Retries: ${stats.retries}`);
1069
+ }
1070
+ if (this.verificationResult) {
1071
+ parts.push(` Verification: ${this.verificationResult.passed ? 'PASSED' : 'FAILED'}`);
1072
+ }
1073
+ return parts.join('\n');
1074
+ }
1075
+ buildErrorResult(message) {
1076
+ return {
1077
+ success: false,
1078
+ summary: `Swarm failed: ${message}`,
1079
+ tasks: this.taskQueue.getAllTasks(),
1080
+ stats: this.buildStats(),
1081
+ errors: this.errors,
1082
+ };
1083
+ }
1084
+ /** Parse JSON from LLM response, handling markdown code blocks. */
1085
+ parseJSON(content) {
1086
+ try {
1087
+ // Strip markdown code blocks if present
1088
+ let json = content;
1089
+ const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
1090
+ if (codeBlockMatch) {
1091
+ json = codeBlockMatch[1];
1092
+ }
1093
+ return JSON.parse(json);
1094
+ }
1095
+ catch {
1096
+ return null;
1097
+ }
1098
+ }
1099
+ /** Get a model health summary for emitting events. */
1100
+ getModelHealthSummary(model) {
1101
+ const records = this.healthTracker.getAllRecords();
1102
+ const record = records.find(r => r.model === model);
1103
+ return record
1104
+ ? { successes: record.successes, failures: record.failures, rateLimits: record.rateLimits, lastRateLimit: record.lastRateLimit, averageLatencyMs: record.averageLatencyMs, healthy: record.healthy }
1105
+ : { successes: 0, failures: 0, rateLimits: 0, averageLatencyMs: 0, healthy: true };
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Factory function.
1110
+ */
1111
+ export function createSwarmOrchestrator(config, provider, agentRegistry, spawnAgentFn, blackboard) {
1112
+ return new SwarmOrchestrator(config, provider, agentRegistry, spawnAgentFn, blackboard);
1113
+ }
1114
+ //# sourceMappingURL=swarm-orchestrator.js.map