@synergenius/flow-weaver-pack-weaver 0.9.59 → 0.9.77

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 (217) hide show
  1. package/dist/ai-chat-provider.d.ts +12 -0
  2. package/dist/ai-chat-provider.d.ts.map +1 -1
  3. package/dist/ai-chat-provider.js +351 -335
  4. package/dist/ai-chat-provider.js.map +1 -1
  5. package/dist/bot/agent-loop.d.ts +20 -0
  6. package/dist/bot/agent-loop.d.ts.map +1 -0
  7. package/dist/bot/agent-loop.js +331 -0
  8. package/dist/bot/agent-loop.js.map +1 -0
  9. package/dist/bot/ai-router.d.ts +19 -0
  10. package/dist/bot/ai-router.d.ts.map +1 -0
  11. package/dist/bot/ai-router.js +104 -0
  12. package/dist/bot/ai-router.js.map +1 -0
  13. package/dist/bot/assistant-tools.d.ts.map +1 -1
  14. package/dist/bot/assistant-tools.js +49 -33
  15. package/dist/bot/assistant-tools.js.map +1 -1
  16. package/dist/bot/async-mutex.d.ts +13 -0
  17. package/dist/bot/async-mutex.d.ts.map +1 -0
  18. package/dist/bot/async-mutex.js +37 -0
  19. package/dist/bot/async-mutex.js.map +1 -0
  20. package/dist/bot/bot-manager.d.ts +2 -2
  21. package/dist/bot/bot-manager.d.ts.map +1 -1
  22. package/dist/bot/bot-manager.js +3 -3
  23. package/dist/bot/bot-manager.js.map +1 -1
  24. package/dist/bot/bot-registry.js +2 -2
  25. package/dist/bot/bot-registry.js.map +1 -1
  26. package/dist/bot/conversation-store.d.ts +1 -0
  27. package/dist/bot/conversation-store.d.ts.map +1 -1
  28. package/dist/bot/conversation-store.js.map +1 -1
  29. package/dist/bot/dashboard.d.ts.map +1 -1
  30. package/dist/bot/dashboard.js +17 -8
  31. package/dist/bot/dashboard.js.map +1 -1
  32. package/dist/bot/improve-loop.js.map +1 -1
  33. package/dist/bot/index.d.ts +2 -4
  34. package/dist/bot/index.d.ts.map +1 -1
  35. package/dist/bot/index.js +1 -2
  36. package/dist/bot/index.js.map +1 -1
  37. package/dist/bot/instance-manager.d.ts +31 -0
  38. package/dist/bot/instance-manager.d.ts.map +1 -0
  39. package/dist/bot/instance-manager.js +115 -0
  40. package/dist/bot/instance-manager.js.map +1 -0
  41. package/dist/bot/orchestrator.d.ts +36 -0
  42. package/dist/bot/orchestrator.d.ts.map +1 -0
  43. package/dist/bot/orchestrator.js +176 -0
  44. package/dist/bot/orchestrator.js.map +1 -0
  45. package/dist/bot/profile-store.d.ts +36 -0
  46. package/dist/bot/profile-store.d.ts.map +1 -0
  47. package/dist/bot/profile-store.js +208 -0
  48. package/dist/bot/profile-store.js.map +1 -0
  49. package/dist/bot/profile-types.d.ts +126 -0
  50. package/dist/bot/profile-types.d.ts.map +1 -0
  51. package/dist/bot/profile-types.js +7 -0
  52. package/dist/bot/profile-types.js.map +1 -0
  53. package/dist/bot/run-store.d.ts.map +1 -1
  54. package/dist/bot/run-store.js +8 -0
  55. package/dist/bot/run-store.js.map +1 -1
  56. package/dist/bot/runner.d.ts +4 -0
  57. package/dist/bot/runner.d.ts.map +1 -1
  58. package/dist/bot/runner.js +5 -1
  59. package/dist/bot/runner.js.map +1 -1
  60. package/dist/bot/swarm-controller.d.ts +109 -0
  61. package/dist/bot/swarm-controller.d.ts.map +1 -0
  62. package/dist/bot/swarm-controller.js +640 -0
  63. package/dist/bot/swarm-controller.js.map +1 -0
  64. package/dist/bot/swarm-event-log.d.ts +28 -0
  65. package/dist/bot/swarm-event-log.d.ts.map +1 -0
  66. package/dist/bot/swarm-event-log.js +54 -0
  67. package/dist/bot/swarm-event-log.js.map +1 -0
  68. package/dist/bot/task-prompt-builder.d.ts +22 -0
  69. package/dist/bot/task-prompt-builder.d.ts.map +1 -0
  70. package/dist/bot/task-prompt-builder.js +240 -0
  71. package/dist/bot/task-prompt-builder.js.map +1 -0
  72. package/dist/bot/task-store.d.ts +21 -0
  73. package/dist/bot/task-store.d.ts.map +1 -0
  74. package/dist/bot/task-store.js +364 -0
  75. package/dist/bot/task-store.js.map +1 -0
  76. package/dist/bot/task-types.d.ts +79 -0
  77. package/dist/bot/task-types.d.ts.map +1 -0
  78. package/dist/bot/task-types.js +6 -0
  79. package/dist/bot/task-types.js.map +1 -0
  80. package/dist/bot/types.d.ts +8 -0
  81. package/dist/bot/types.d.ts.map +1 -1
  82. package/dist/cli-handlers.d.ts.map +1 -1
  83. package/dist/cli-handlers.js +79 -54
  84. package/dist/cli-handlers.js.map +1 -1
  85. package/dist/cli.d.ts +3 -0
  86. package/dist/cli.d.ts.map +1 -0
  87. package/dist/cli.js +749 -0
  88. package/dist/cli.js.map +1 -0
  89. package/dist/docs/docs/weaver-bot-usage.md +35 -18
  90. package/dist/docs/docs/weaver-config.md +20 -0
  91. package/dist/docs/docs/weaver-task-queue.md +31 -19
  92. package/dist/docs/weaver-config.md +15 -9
  93. package/dist/index.d.ts +2 -2
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/index.js.map +1 -1
  97. package/dist/mcp-tools.d.ts +17 -0
  98. package/dist/mcp-tools.d.ts.map +1 -1
  99. package/dist/mcp-tools.js +98 -279
  100. package/dist/mcp-tools.js.map +1 -1
  101. package/dist/node-types/bot-report.d.ts.map +1 -1
  102. package/dist/node-types/bot-report.js +6 -24
  103. package/dist/node-types/bot-report.js.map +1 -1
  104. package/dist/node-types/orchestrator-dispatch.d.ts +17 -0
  105. package/dist/node-types/orchestrator-dispatch.d.ts.map +1 -0
  106. package/dist/node-types/orchestrator-dispatch.js +63 -0
  107. package/dist/node-types/orchestrator-dispatch.js.map +1 -0
  108. package/dist/node-types/orchestrator-load-state.d.ts +16 -0
  109. package/dist/node-types/orchestrator-load-state.d.ts.map +1 -0
  110. package/dist/node-types/orchestrator-load-state.js +60 -0
  111. package/dist/node-types/orchestrator-load-state.js.map +1 -0
  112. package/dist/node-types/orchestrator-route.d.ts +16 -0
  113. package/dist/node-types/orchestrator-route.d.ts.map +1 -0
  114. package/dist/node-types/orchestrator-route.js +28 -0
  115. package/dist/node-types/orchestrator-route.js.map +1 -0
  116. package/dist/node-types/receive-task.d.ts +2 -3
  117. package/dist/node-types/receive-task.d.ts.map +1 -1
  118. package/dist/node-types/receive-task.js +3 -48
  119. package/dist/node-types/receive-task.js.map +1 -1
  120. package/dist/templates/weaver-template.d.ts +11 -0
  121. package/dist/templates/weaver-template.d.ts.map +1 -0
  122. package/dist/templates/weaver-template.js +53 -0
  123. package/dist/templates/weaver-template.js.map +1 -0
  124. package/dist/ui/bot-activity.js +2 -2
  125. package/dist/ui/bot-constants.d.ts +14 -0
  126. package/dist/ui/bot-constants.d.ts.map +1 -0
  127. package/dist/ui/bot-constants.js +189 -0
  128. package/dist/ui/bot-constants.js.map +1 -0
  129. package/dist/ui/bot-panel.js +207 -245
  130. package/dist/ui/bot-slot-card.js +141 -0
  131. package/dist/ui/budget-bar.js +59 -0
  132. package/dist/ui/chat-task-result.js +178 -0
  133. package/dist/ui/decision-log.js +136 -0
  134. package/dist/ui/profile-card.js +158 -0
  135. package/dist/ui/profile-editor.js +597 -0
  136. package/dist/ui/swarm-controls.js +245 -0
  137. package/dist/ui/swarm-dashboard.js +3012 -0
  138. package/dist/ui/task-create-form.js +98 -0
  139. package/dist/ui/task-detail-view.js +1044 -0
  140. package/dist/ui/task-pool-list.js +156 -0
  141. package/dist/workflows/orchestrator.d.ts +21 -0
  142. package/dist/workflows/orchestrator.d.ts.map +1 -0
  143. package/dist/workflows/orchestrator.js +281 -0
  144. package/dist/workflows/orchestrator.js.map +1 -0
  145. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  146. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  147. package/dist/workflows/weaver-bot-session.js +68 -0
  148. package/dist/workflows/weaver-bot-session.js.map +1 -0
  149. package/dist/workflows/weaver.d.ts +24 -0
  150. package/dist/workflows/weaver.d.ts.map +1 -0
  151. package/dist/workflows/weaver.js +28 -0
  152. package/dist/workflows/weaver.js.map +1 -0
  153. package/flowweaver.manifest.json +547 -133
  154. package/package.json +1 -1
  155. package/src/ai-chat-provider.ts +378 -371
  156. package/src/bot/ai-router.ts +132 -0
  157. package/src/bot/assistant-tools.ts +47 -29
  158. package/src/bot/async-mutex.ts +37 -0
  159. package/src/bot/bot-manager.ts +3 -3
  160. package/src/bot/bot-registry.ts +2 -2
  161. package/src/bot/conversation-store.ts +2 -1
  162. package/src/bot/dashboard.ts +17 -8
  163. package/src/bot/improve-loop.ts +6 -6
  164. package/src/bot/index.ts +2 -4
  165. package/src/bot/instance-manager.ts +128 -0
  166. package/src/bot/orchestrator.ts +244 -0
  167. package/src/bot/profile-store.ts +225 -0
  168. package/src/bot/profile-types.ts +141 -0
  169. package/src/bot/run-store.ts +8 -0
  170. package/src/bot/runner.ts +9 -1
  171. package/src/bot/swarm-controller.ts +780 -0
  172. package/src/bot/swarm-event-log.ts +57 -0
  173. package/src/bot/task-prompt-builder.ts +309 -0
  174. package/src/bot/task-store.ts +407 -0
  175. package/src/bot/task-types.ts +100 -0
  176. package/src/bot/types.ts +8 -0
  177. package/src/cli-handlers.ts +78 -53
  178. package/src/docs/weaver-bot-usage.md +35 -18
  179. package/src/docs/weaver-config.md +20 -0
  180. package/src/docs/weaver-task-queue.md +31 -19
  181. package/src/index.ts +5 -4
  182. package/src/mcp-tools.ts +129 -372
  183. package/src/node-types/bot-report.ts +6 -24
  184. package/src/node-types/orchestrator-dispatch.ts +71 -0
  185. package/src/node-types/orchestrator-load-state.ts +66 -0
  186. package/src/node-types/orchestrator-route.ts +33 -0
  187. package/src/node-types/receive-task.ts +3 -57
  188. package/src/ui/bot-activity.tsx +2 -2
  189. package/src/ui/bot-constants.ts +192 -0
  190. package/src/ui/bot-panel.tsx +213 -247
  191. package/src/ui/bot-slot-card.tsx +139 -0
  192. package/src/ui/budget-bar.tsx +30 -0
  193. package/src/ui/chat-task-result.tsx +236 -0
  194. package/src/ui/decision-log.tsx +148 -0
  195. package/src/ui/profile-card.tsx +157 -0
  196. package/src/ui/profile-editor.tsx +384 -0
  197. package/src/ui/swarm-controls.tsx +260 -0
  198. package/src/ui/swarm-dashboard.tsx +647 -0
  199. package/src/ui/task-create-form.tsx +87 -0
  200. package/src/ui/task-detail-view.tsx +841 -0
  201. package/src/ui/task-pool-list.tsx +187 -0
  202. package/src/workflows/orchestrator.ts +302 -0
  203. package/dist/docs/weaver-bot-usage.md +0 -34
  204. package/dist/docs/weaver-genesis.md +0 -32
  205. package/dist/docs/weaver-task-queue.md +0 -34
  206. package/dist/ui/bot-workspace.js +0 -1015
  207. package/dist/ui/chat-bot-result.js +0 -71
  208. package/dist/ui/queue-input.js +0 -82
  209. package/dist/ui/session-bar.js +0 -174
  210. package/src/bot/error-guide.ts +0 -4
  211. package/src/bot/retry-utils.ts +0 -4
  212. package/src/bot/session-state.ts +0 -116
  213. package/src/bot/task-queue.ts +0 -262
  214. package/src/ui/bot-workspace.tsx +0 -442
  215. package/src/ui/chat-bot-result.tsx +0 -81
  216. package/src/ui/queue-input.tsx +0 -56
  217. package/src/ui/session-bar.tsx +0 -157
@@ -0,0 +1,780 @@
1
+ /**
2
+ * SwarmController — multi-bot orchestrator for the Weaver Swarm.
3
+ *
4
+ * Uses a centralized orchestrator dispatch loop instead of per-bot pull loops.
5
+ * The Orchestrator routes pending tasks to the best available bot instance,
6
+ * the InstanceManager handles scaling, and the ProfileStore persists profiles.
7
+ *
8
+ * Singleton per workspace via `SwarmController.getInstance(projectDir)`.
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { TaskStore } from './task-store.js';
14
+ import { BotRegistry } from './bot-registry.js';
15
+ import type { BotRegistration } from './bot-registry.js';
16
+ import { SwarmEventLog } from './swarm-event-log.js';
17
+ import { RunStore } from './run-store.js';
18
+ import { EventLog } from './event-log.js';
19
+ import { runRegistry } from './run-registry.js';
20
+ import { runWorkflow } from './runner.js';
21
+ import { buildTaskPrompt } from './task-prompt-builder.js';
22
+ import { Orchestrator } from './orchestrator.js';
23
+ import { AIRouterImpl } from './ai-router.js';
24
+ import { InstanceManager } from './instance-manager.js';
25
+ import { ProfileStore } from './profile-store.js';
26
+ import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision } from './profile-types.js';
27
+ import type { Task, RunSummary } from './task-types.js';
28
+ import type { WorkflowResult } from './types.js';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Types
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export interface SwarmBudgets {
35
+ workspace: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
36
+ session: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
37
+ }
38
+
39
+ export interface SwarmState {
40
+ status: 'idle' | 'running' | 'paused' | 'stopping';
41
+ startedAt?: string;
42
+
43
+ /** Active bot instances managed by InstanceManager. */
44
+ instances: Record<string, BotInstance>;
45
+ maxConcurrent: number;
46
+
47
+ budgets: SwarmBudgets;
48
+
49
+ autoRetry: boolean;
50
+ maxAttemptsDefault: number;
51
+
52
+ // Stats
53
+ tasksCompleted: number;
54
+ tasksFailed: number;
55
+ totalTokensUsed: number;
56
+ totalCost: number;
57
+ }
58
+
59
+ export interface SwarmConfig {
60
+ maxConcurrent?: number;
61
+ sessionBudgetTokens?: number;
62
+ sessionBudgetCost?: number;
63
+ workspaceBudgetTokens?: number;
64
+ workspaceBudgetCost?: number;
65
+ autoRetry?: boolean;
66
+ maxAttemptsDefault?: number;
67
+ }
68
+
69
+ export interface SwarmStartConfig {
70
+ maxConcurrent?: number;
71
+ sessionBudgetTokens?: number;
72
+ sessionBudgetCost?: number;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Constants
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const DISPATCH_LOOP_SLEEP_MS = 2000;
80
+ const SWARM_STATE_FILE = 'swarm.json';
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // SwarmController
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export class SwarmController {
87
+ private static instances = new Map<string, SwarmController>();
88
+
89
+ private readonly projectDir: string;
90
+ private readonly weaverDir: string;
91
+ private readonly statePath: string;
92
+ private readonly taskStore: TaskStore;
93
+ private readonly eventLog: SwarmEventLog;
94
+ private readonly orchestrator: Orchestrator;
95
+ private readonly instanceManager: InstanceManager;
96
+ private readonly profileStore: ProfileStore;
97
+
98
+ private state: SwarmState;
99
+
100
+ /** Abort controller for the single dispatch loop. */
101
+ private dispatchAbort: AbortController | null = null;
102
+ /** Promise for the dispatch loop (resolved when the loop exits). */
103
+ private dispatchLoopPromise: Promise<void> | null = null;
104
+ /** Promises for currently executing tasks (instanceId -> Promise). */
105
+ private executionPromises = new Map<string, Promise<void>>();
106
+
107
+ /** Map of profileId -> BotRegistration for workflow execution. */
108
+ private profileBotMap = new Map<string, BotRegistration>();
109
+
110
+ // -----------------------------------------------------------------------
111
+ // Singleton
112
+ // -----------------------------------------------------------------------
113
+
114
+ static getInstance(projectDir: string): SwarmController {
115
+ const key = path.resolve(projectDir);
116
+ let inst = SwarmController.instances.get(key);
117
+ if (!inst) {
118
+ inst = new SwarmController(key);
119
+ SwarmController.instances.set(key, inst);
120
+ }
121
+ return inst;
122
+ }
123
+
124
+ /** Clear all cached instances (used by tests). */
125
+ static clearInstances(): void {
126
+ SwarmController.instances.clear();
127
+ }
128
+
129
+ private constructor(projectDir: string) {
130
+ this.projectDir = projectDir;
131
+ this.weaverDir = path.join(projectDir, '.weaver');
132
+ if (!fs.existsSync(this.weaverDir)) fs.mkdirSync(this.weaverDir, { recursive: true });
133
+ this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
134
+ this.taskStore = new TaskStore(projectDir);
135
+ this.eventLog = new SwarmEventLog(projectDir);
136
+ this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
137
+ this.instanceManager = new InstanceManager();
138
+ this.profileStore = new ProfileStore(projectDir);
139
+
140
+ // Load persisted state or create default
141
+ this.state = this._loadState();
142
+
143
+ // Crash recovery: no dispatch loop survives a process restart, so reset
144
+ // any stale active status back to idle so start() is not short-circuited.
145
+ if (this.state.status === 'running' || this.state.status === 'paused' || this.state.status === 'stopping') {
146
+ this.state.status = 'idle';
147
+ }
148
+
149
+ this._persist();
150
+ }
151
+
152
+ // -----------------------------------------------------------------------
153
+ // Public API
154
+ // -----------------------------------------------------------------------
155
+
156
+ async start(config?: SwarmStartConfig): Promise<void> {
157
+ if (this.state.status === 'running') return;
158
+
159
+ // Apply start config
160
+ if (config?.maxConcurrent !== undefined) this.state.maxConcurrent = config.maxConcurrent;
161
+ if (config?.sessionBudgetTokens !== undefined) this.state.budgets.session.limitTokens = config.sessionBudgetTokens;
162
+ if (config?.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
163
+
164
+ // Reset session budget counters on new start
165
+ this.state.budgets.session.usedTokens = 0;
166
+ this.state.budgets.session.usedCost = 0;
167
+
168
+ // Discover bots from registry
169
+ const registry = new BotRegistry(this.projectDir);
170
+ const bots = registry.list();
171
+
172
+ // Ensure default profiles exist for all registered bots
173
+ this.profileStore.ensureDefaultProfiles(
174
+ bots.map((b) => ({ id: b.id, name: b.name, icon: b.icon, color: b.color })),
175
+ );
176
+
177
+ // Load profiles and build profile-to-bot mapping
178
+ const profiles = this.profileStore.list();
179
+ this.profileBotMap.clear();
180
+ for (const profile of profiles) {
181
+ const bot = bots.find((b) => b.id === profile.botId);
182
+ if (bot) this.profileBotMap.set(profile.id, bot);
183
+ }
184
+
185
+ // Stop any existing instances
186
+ this.instanceManager.stopAll();
187
+
188
+ // Spawn initial instances per profile (up to maxConcurrent total)
189
+ let totalSpawned = 0;
190
+ for (const profile of profiles) {
191
+ if (totalSpawned >= this.state.maxConcurrent) break;
192
+
193
+ // Spawn at least minInstances, or 1 if minInstances is 0
194
+ const toSpawn = Math.max(profile.minInstances, 1);
195
+ const clamped = Math.min(toSpawn, this.state.maxConcurrent - totalSpawned);
196
+
197
+ for (let i = 0; i < clamped; i++) {
198
+ this.instanceManager.spawn(profile);
199
+ totalSpawned++;
200
+ }
201
+ }
202
+
203
+ // Populate state fields
204
+ this._syncInstancesState();
205
+
206
+ this.state.status = 'running';
207
+ this.state.startedAt = new Date().toISOString();
208
+ this._persist();
209
+
210
+ this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now() });
211
+
212
+ // Start the centralized dispatch loop
213
+ this._startDispatchLoop();
214
+ }
215
+
216
+ async pause(): Promise<void> {
217
+ if (this.state.status !== 'running') return;
218
+
219
+ this.state.status = 'paused';
220
+ this._persist();
221
+
222
+ this.eventLog.emit({ type: 'swarm-paused', timestamp: Date.now() });
223
+
224
+ // The dispatch loop will check paused status and skip routing
225
+ }
226
+
227
+ async stop(): Promise<void> {
228
+ if (this.state.status === 'idle') return;
229
+
230
+ this.state.status = 'stopping';
231
+ this._persist();
232
+
233
+ // Signal the dispatch loop to stop
234
+ if (this.dispatchAbort) {
235
+ this.dispatchAbort.abort();
236
+ }
237
+
238
+ // Wait for the dispatch loop to finish
239
+ if (this.dispatchLoopPromise) {
240
+ await this.dispatchLoopPromise;
241
+ }
242
+
243
+ // Wait for all running executions to finish
244
+ const execPromises = Array.from(this.executionPromises.values());
245
+ await Promise.allSettled(execPromises);
246
+
247
+ // Stop all instances
248
+ this.instanceManager.stopAll();
249
+
250
+ // Update state
251
+ this._syncInstancesState();
252
+
253
+ this.state.status = 'idle';
254
+ this._persist();
255
+
256
+ this.dispatchAbort = null;
257
+ this.dispatchLoopPromise = null;
258
+ this.executionPromises.clear();
259
+
260
+ this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
261
+ }
262
+
263
+ getStatus(): SwarmState {
264
+ // Sync instances from InstanceManager before returning
265
+ this._syncInstancesState();
266
+ return { ...this.state, instances: { ...this.state.instances } };
267
+ }
268
+
269
+ configure(config: SwarmConfig): void {
270
+ if (config.maxConcurrent !== undefined) this.state.maxConcurrent = config.maxConcurrent;
271
+ if (config.sessionBudgetTokens !== undefined) this.state.budgets.session.limitTokens = config.sessionBudgetTokens;
272
+ if (config.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
273
+ if (config.workspaceBudgetTokens !== undefined) this.state.budgets.workspace.limitTokens = config.workspaceBudgetTokens;
274
+ if (config.workspaceBudgetCost !== undefined) this.state.budgets.workspace.limitCost = config.workspaceBudgetCost;
275
+ if (config.autoRetry !== undefined) this.state.autoRetry = config.autoRetry;
276
+ if (config.maxAttemptsDefault !== undefined) this.state.maxAttemptsDefault = config.maxAttemptsDefault;
277
+ this._persist();
278
+ }
279
+
280
+ // -----------------------------------------------------------------------
281
+ // Crash recovery
282
+ // -----------------------------------------------------------------------
283
+
284
+ async recover(): Promise<void> {
285
+ // Reset all instances to idle
286
+ const allInstances = this.instanceManager.listAll();
287
+ for (const inst of allInstances) {
288
+ if (inst.status === 'executing') {
289
+ this.instanceManager.markIdle(inst.instanceId, false);
290
+ }
291
+ }
292
+
293
+ // Reset in-progress tasks
294
+ const tasks = await this.taskStore.list({ status: 'in-progress' });
295
+ for (const task of tasks) {
296
+ if (this.state.autoRetry && task.attempt < task.maxAttempts) {
297
+ await this.taskStore.update(task.id, {
298
+ status: 'pending',
299
+ currentBotId: undefined,
300
+ });
301
+ } else {
302
+ await this.taskStore.update(task.id, {
303
+ status: 'failed',
304
+ currentBotId: undefined,
305
+ });
306
+ }
307
+ }
308
+
309
+ // Reset swarm status to idle
310
+ this.state.status = 'idle';
311
+ this._syncInstancesState();
312
+ this._persist();
313
+ }
314
+
315
+ // -----------------------------------------------------------------------
316
+ // Token/cost recording
317
+ // -----------------------------------------------------------------------
318
+
319
+ recordTokenUsage(instanceId: string, _taskId: string, tokensUsed: number, cost: number): void {
320
+ // Update instance in InstanceManager
321
+ try {
322
+ this.instanceManager.recordUsage(instanceId, tokensUsed, cost);
323
+ } catch { /* instance may not exist if stopped */ }
324
+
325
+ // Update session budgets
326
+ this.state.budgets.session.usedTokens += tokensUsed;
327
+ this.state.budgets.session.usedCost += cost;
328
+
329
+ // Update workspace budgets
330
+ this.state.budgets.workspace.usedTokens += tokensUsed;
331
+ this.state.budgets.workspace.usedCost += cost;
332
+
333
+ // Update totals
334
+ this.state.totalTokensUsed += tokensUsed;
335
+ this.state.totalCost += cost;
336
+
337
+ this._persist();
338
+
339
+ this.eventLog.emit({
340
+ type: 'budget-updated',
341
+ timestamp: Date.now(),
342
+ data: { botId: instanceId, tokensUsed, cost },
343
+ });
344
+ }
345
+
346
+ // -----------------------------------------------------------------------
347
+ // Orchestrator accessors
348
+ // -----------------------------------------------------------------------
349
+
350
+ getProfileStore(): ProfileStore {
351
+ return this.profileStore;
352
+ }
353
+
354
+ getInstanceManager(): InstanceManager {
355
+ return this.instanceManager;
356
+ }
357
+
358
+ getOrchestrator(): Orchestrator {
359
+ return this.orchestrator;
360
+ }
361
+
362
+ getOrchestratorStatus(): { decisions: OrchestratorDecision[]; stats: { totalDecisions: number; assignmentsByMethod: Record<string, number> } } {
363
+ const decisions = this.orchestrator.getDecisionLog();
364
+ const stats = {
365
+ totalDecisions: decisions.length,
366
+ assignmentsByMethod: {} as Record<string, number>,
367
+ };
368
+ for (const d of decisions) {
369
+ stats.assignmentsByMethod[d.method] = (stats.assignmentsByMethod[d.method] ?? 0) + 1;
370
+ }
371
+ return { decisions, stats };
372
+ }
373
+
374
+ // -----------------------------------------------------------------------
375
+ // Dispatch loop
376
+ // -----------------------------------------------------------------------
377
+
378
+ private _startDispatchLoop(): void {
379
+ const abort = new AbortController();
380
+ this.dispatchAbort = abort;
381
+
382
+ this.dispatchLoopPromise = this._dispatchLoop(abort.signal).catch(() => {
383
+ // Swallow errors in dispatch loop — self-heals on next cycle
384
+ });
385
+ }
386
+
387
+ private async _dispatchLoop(signal: AbortSignal): Promise<void> {
388
+ while (!signal.aborted) {
389
+ // Check if swarm is still running
390
+ if (this.state.status === 'stopping' || this.state.status === 'idle') break;
391
+
392
+ // Check if paused — sleep and try again
393
+ if (this.state.status === 'paused') {
394
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
395
+ continue;
396
+ }
397
+
398
+ // Budget check before routing
399
+ if (this._isBudgetExceeded()) {
400
+ this.state.status = 'paused';
401
+ this._persist();
402
+ this.eventLog.emit({
403
+ type: 'swarm-paused',
404
+ timestamp: Date.now(),
405
+ data: { reason: 'budget-exceeded' },
406
+ });
407
+ break;
408
+ }
409
+
410
+ // Check instance-level steering
411
+ const allInstances = this.instanceManager.listAll();
412
+ for (const inst of allInstances) {
413
+ await this._checkSteering(inst.instanceId);
414
+ }
415
+
416
+ // Collect pending tasks (and all tasks for dependency checks)
417
+ const pendingTasks = await this.taskStore.list({ status: ['pending', 'failed'] });
418
+ const allTasks = await this.taskStore.list();
419
+ const routableTasks = pendingTasks.filter((t) => {
420
+ // Skip parent tasks
421
+ if (t.isParent) return false;
422
+ // Skip tasks that have exhausted retries
423
+ if (t.status === 'failed' && t.attempt >= t.maxAttempts) return false;
424
+ // Skip tasks over per-task budget
425
+ if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens) return false;
426
+ if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost) return false;
427
+ // Skip tasks whose dependencies are not all done
428
+ if (t.dependsOn && t.dependsOn.length > 0) {
429
+ const allDepsDone = t.dependsOn.every((depId) => {
430
+ const dep = allTasks.find((d) => d.id === depId);
431
+ return dep && dep.status === 'done';
432
+ });
433
+ if (!allDepsDone) return false;
434
+ }
435
+ return true;
436
+ });
437
+
438
+ if (routableTasks.length === 0) {
439
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
440
+ continue;
441
+ }
442
+
443
+ // Build orchestrator input
444
+ const profiles = this.profileStore.list();
445
+ const instances = this.instanceManager.listAll();
446
+ const { workspace, session } = this.state.budgets;
447
+
448
+ const budgetRemaining = {
449
+ tokens: this._remainingBudget(session.limitTokens, session.usedTokens, workspace.limitTokens, workspace.usedTokens),
450
+ cost: this._remainingBudget(session.limitCost, session.usedCost, workspace.limitCost, workspace.usedCost),
451
+ };
452
+
453
+ const input: OrchestratorInput = {
454
+ pendingTasks: routableTasks.map((t) => ({
455
+ id: t.id,
456
+ title: t.title,
457
+ description: t.description,
458
+ priority: t.priority,
459
+ complexity: t.complexity ?? 'moderate',
460
+ assignedProfile: t.assignedProfile,
461
+ context: {
462
+ runSummaries: t.context.runSummaries.map((rs) => ({
463
+ outcome: rs.outcome,
464
+ botId: rs.botId,
465
+ })),
466
+ },
467
+ })),
468
+ profiles,
469
+ instances,
470
+ budgetRemaining,
471
+ };
472
+
473
+ // Route
474
+ const output = await this.orchestrator.route(input);
475
+
476
+ // Apply scale actions
477
+ for (const sa of output.scaleActions) {
478
+ const profile = profiles.find((p) => p.id === sa.profileId);
479
+ if (profile) {
480
+ this.instanceManager.scaleTo(profile, sa.targetInstances);
481
+ }
482
+ }
483
+
484
+ // Apply assignments
485
+ for (const assignment of output.assignments) {
486
+ const profile = profiles.find((p) => p.id === assignment.profileId);
487
+ const bot = this.profileBotMap.get(assignment.profileId);
488
+ if (!profile || !bot) continue;
489
+
490
+ try {
491
+ // Assign task in TaskStore
492
+ await this.taskStore.assignToInstance(assignment.taskId, assignment.instanceId, assignment.profileId);
493
+
494
+ // Mark instance as executing and store runId on task for live streaming
495
+ const runId = RunStore.newId();
496
+ this.instanceManager.markExecuting(assignment.instanceId, assignment.taskId, runId);
497
+ await this.taskStore.update(assignment.taskId, { currentRunId: runId });
498
+
499
+ // Sync state for dashboard
500
+ this._syncInstancesState();
501
+ this._persist();
502
+
503
+ this.eventLog.emit({
504
+ type: 'task-claimed',
505
+ timestamp: Date.now(),
506
+ data: { botId: assignment.instanceId, taskId: assignment.taskId, profileId: assignment.profileId, method: assignment.method },
507
+ });
508
+
509
+ // Spawn execution (non-blocking)
510
+ const execPromise = this._executeTask(assignment.instanceId, assignment.taskId, runId, bot, profile).catch(() => {
511
+ // errors handled inside _executeTask
512
+ });
513
+ this.executionPromises.set(assignment.instanceId, execPromise);
514
+ } catch {
515
+ // Assignment failed (task may have been grabbed by another cycle) — skip
516
+ }
517
+ }
518
+
519
+ // Wait for completions via Promise.race with a sleep timeout
520
+ if (this.executionPromises.size > 0) {
521
+ const sleepPromise = this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
522
+ await Promise.race([
523
+ Promise.race(Array.from(this.executionPromises.values())),
524
+ sleepPromise,
525
+ ]);
526
+ } else {
527
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
528
+ }
529
+
530
+ // Clean up completed execution promises
531
+ for (const [instanceId, promise] of this.executionPromises) {
532
+ // Check if resolved by racing with an immediately resolved promise
533
+ const result = await Promise.race([
534
+ promise.then(() => 'done'),
535
+ Promise.resolve('pending'),
536
+ ]);
537
+ if (result === 'done') {
538
+ this.executionPromises.delete(instanceId);
539
+ }
540
+ }
541
+
542
+ // Post-cycle budget check
543
+ if (this._isBudgetExceeded()) {
544
+ this.state.status = 'paused';
545
+ this._persist();
546
+ this.eventLog.emit({
547
+ type: 'swarm-paused',
548
+ timestamp: Date.now(),
549
+ data: { reason: 'budget-exceeded' },
550
+ });
551
+ break;
552
+ }
553
+
554
+ this._syncInstancesState();
555
+ this._persist();
556
+ }
557
+ }
558
+
559
+ // -----------------------------------------------------------------------
560
+ // Task execution
561
+ // -----------------------------------------------------------------------
562
+
563
+ private async _executeTask(
564
+ instanceId: string,
565
+ taskId: string,
566
+ runId: string,
567
+ bot: BotRegistration,
568
+ profile: BotProfile,
569
+ ): Promise<void> {
570
+ const startTime = Date.now();
571
+ let result: WorkflowResult;
572
+
573
+ try {
574
+ const task = await this.taskStore.get(taskId);
575
+ if (!task) throw new Error(`Task not found: ${taskId}`);
576
+
577
+ // Build prompt from task context
578
+ const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
579
+ const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
580
+ const prompt = buildTaskPrompt(task, parentTask, siblingTasks, profile.preferences);
581
+
582
+ // Create per-run event log
583
+ const runEventLog = new EventLog(runId);
584
+
585
+ // Execute workflow
586
+ result = await runWorkflow(bot.filePath, {
587
+ runId,
588
+ taskId,
589
+ botId: instanceId,
590
+ config: { provider: 'auto' },
591
+ params: { taskJson: prompt, projectDir: this.projectDir },
592
+ eventLog: runEventLog,
593
+ });
594
+
595
+ runEventLog.done();
596
+ } catch (err) {
597
+ result = {
598
+ success: false,
599
+ summary: (err instanceof Error ? err.message : String(err)),
600
+ outcome: 'error',
601
+ };
602
+ }
603
+
604
+ const durationMs = Date.now() - startTime;
605
+
606
+ // Extract run summary
607
+ const tokensUsed = Math.max(0, (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0));
608
+ const costUsed = Math.max(0, result.cost?.totalCost ?? 0);
609
+
610
+ const runSummary: RunSummary = {
611
+ runId,
612
+ botId: instanceId,
613
+ outcome: result.success ? 'success' : 'failed',
614
+ summary: result.summary || (result.success ? 'Completed' : 'Failed'),
615
+ filesModified: [],
616
+ error: result.success ? undefined : (result.summary || 'Unknown error'),
617
+ durationMs,
618
+ tokensUsed,
619
+ cost: costUsed,
620
+ };
621
+
622
+ // Release task
623
+ const releaseStatus = result.success ? 'done' : 'failed';
624
+ await this.taskStore.release(taskId, releaseStatus, runSummary);
625
+
626
+ // Record token usage
627
+ this.recordTokenUsage(instanceId, taskId, tokensUsed, costUsed);
628
+
629
+ // Update stats
630
+ if (result.success) {
631
+ this.state.tasksCompleted += 1;
632
+ } else {
633
+ this.state.tasksFailed += 1;
634
+ }
635
+
636
+ // Mark instance as idle
637
+ try {
638
+ this.instanceManager.markIdle(instanceId, result.success);
639
+ } catch { /* instance may have been stopped */ }
640
+
641
+ // Emit task event
642
+ this.eventLog.emit({
643
+ type: result.success ? 'task-done' : 'task-failed',
644
+ timestamp: Date.now(),
645
+ data: { botId: instanceId, taskId, outcome: runSummary.outcome },
646
+ });
647
+
648
+ this._syncInstancesState();
649
+ this._persist();
650
+ }
651
+
652
+ // -----------------------------------------------------------------------
653
+ // Budget checks
654
+ // -----------------------------------------------------------------------
655
+
656
+ private _isBudgetExceeded(): boolean {
657
+ const { workspace, session } = this.state.budgets;
658
+
659
+ // Check session budget
660
+ if (session.limitTokens > 0 && session.usedTokens >= session.limitTokens) return true;
661
+ if (session.limitCost > 0 && session.usedCost >= session.limitCost) return true;
662
+
663
+ // Check workspace budget
664
+ if (workspace.limitTokens > 0 && workspace.usedTokens >= workspace.limitTokens) return true;
665
+ if (workspace.limitCost > 0 && workspace.usedCost >= workspace.limitCost) return true;
666
+
667
+ return false;
668
+ }
669
+
670
+ /** Calculate remaining budget across session and workspace. */
671
+ private _remainingBudget(
672
+ sessionLimit: number,
673
+ sessionUsed: number,
674
+ workspaceLimit: number,
675
+ workspaceUsed: number,
676
+ ): number {
677
+ const remaining: number[] = [];
678
+ if (sessionLimit > 0) remaining.push(sessionLimit - sessionUsed);
679
+ if (workspaceLimit > 0) remaining.push(workspaceLimit - workspaceUsed);
680
+ if (remaining.length === 0) return Number.MAX_SAFE_INTEGER;
681
+ return Math.min(...remaining);
682
+ }
683
+
684
+ // -----------------------------------------------------------------------
685
+ // Steering
686
+ // -----------------------------------------------------------------------
687
+
688
+ private async _checkSteering(instanceId: string): Promise<void> {
689
+ const steerPath = path.join(this.weaverDir, `steer-${instanceId}.json`);
690
+ try {
691
+ if (!fs.existsSync(steerPath)) return;
692
+ const raw = fs.readFileSync(steerPath, 'utf-8');
693
+ fs.unlinkSync(steerPath); // consume the command
694
+ const command = JSON.parse(raw);
695
+
696
+ switch (command.command) {
697
+ case 'pause':
698
+ this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId: instanceId } });
699
+ break;
700
+ case 'resume':
701
+ break;
702
+ case 'cancel':
703
+ this.instanceManager.stop(instanceId);
704
+ break;
705
+ }
706
+ } catch { /* steer file read error — skip */ }
707
+ }
708
+
709
+ // -----------------------------------------------------------------------
710
+ // State sync — instances field
711
+ // -----------------------------------------------------------------------
712
+
713
+ private _syncInstancesState(): void {
714
+ const allInstances = this.instanceManager.listAll();
715
+
716
+ // Build instances record
717
+ this.state.instances = {};
718
+ for (const inst of allInstances) {
719
+ this.state.instances[inst.instanceId] = inst;
720
+ }
721
+ }
722
+
723
+ // -----------------------------------------------------------------------
724
+ // State persistence
725
+ // -----------------------------------------------------------------------
726
+
727
+ private _loadState(): SwarmState {
728
+ try {
729
+ if (fs.existsSync(this.statePath)) {
730
+ const raw = fs.readFileSync(this.statePath, 'utf-8');
731
+ const data = JSON.parse(raw) as SwarmState;
732
+ if (!data.instances) data.instances = {};
733
+ return data;
734
+ }
735
+ } catch { /* corrupt file — use defaults */ }
736
+ return this._defaultState();
737
+ }
738
+
739
+ private _defaultState(): SwarmState {
740
+ return {
741
+ status: 'idle',
742
+ instances: {},
743
+ maxConcurrent: 3,
744
+ budgets: {
745
+ workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
746
+ session: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
747
+ },
748
+ autoRetry: true,
749
+ maxAttemptsDefault: 3,
750
+ tasksCompleted: 0,
751
+ tasksFailed: 0,
752
+ totalTokensUsed: 0,
753
+ totalCost: 0,
754
+ };
755
+ }
756
+
757
+ private _persist(): void {
758
+ try {
759
+ const tmpPath = this.statePath + '.tmp';
760
+ fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2), 'utf-8');
761
+ fs.renameSync(tmpPath, this.statePath);
762
+ } catch (err) {
763
+ console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
764
+ }
765
+ }
766
+
767
+ // -----------------------------------------------------------------------
768
+ // Utilities
769
+ // -----------------------------------------------------------------------
770
+
771
+ private _sleep(ms: number, signal?: AbortSignal): Promise<void> {
772
+ return new Promise((resolve) => {
773
+ const timer = setTimeout(resolve, ms);
774
+ signal?.addEventListener('abort', () => {
775
+ clearTimeout(timer);
776
+ resolve();
777
+ }, { once: true });
778
+ });
779
+ }
780
+ }