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