@synergenius/flow-weaver-pack-weaver 0.9.62 → 0.9.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-chat-provider.d.ts +12 -0
- package/dist/ai-chat-provider.d.ts.map +1 -1
- package/dist/ai-chat-provider.js +173 -19
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/ai-router.d.ts +19 -0
- package/dist/bot/ai-router.d.ts.map +1 -0
- package/dist/bot/ai-router.js +104 -0
- package/dist/bot/ai-router.js.map +1 -0
- package/dist/bot/bot-registry.js +2 -2
- package/dist/bot/bot-registry.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +1 -0
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/improve-loop.js.map +1 -1
- package/dist/bot/instance-manager.d.ts +31 -0
- package/dist/bot/instance-manager.d.ts.map +1 -0
- package/dist/bot/instance-manager.js +115 -0
- package/dist/bot/instance-manager.js.map +1 -0
- package/dist/bot/orchestrator.d.ts +36 -0
- package/dist/bot/orchestrator.d.ts.map +1 -0
- package/dist/bot/orchestrator.js +176 -0
- package/dist/bot/orchestrator.js.map +1 -0
- package/dist/bot/profile-store.d.ts +36 -0
- package/dist/bot/profile-store.d.ts.map +1 -0
- package/dist/bot/profile-store.js +208 -0
- package/dist/bot/profile-store.js.map +1 -0
- package/dist/bot/profile-types.d.ts +126 -0
- package/dist/bot/profile-types.d.ts.map +1 -0
- package/dist/bot/profile-types.js +7 -0
- package/dist/bot/profile-types.js.map +1 -0
- package/dist/bot/session-state.d.ts +25 -0
- package/dist/bot/session-state.d.ts.map +1 -0
- package/dist/bot/session-state.js +110 -0
- package/dist/bot/session-state.js.map +1 -0
- package/dist/bot/swarm-controller.d.ts +37 -21
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +344 -163
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-prompt-builder.d.ts +2 -1
- package/dist/bot/task-prompt-builder.d.ts.map +1 -1
- package/dist/bot/task-prompt-builder.js +33 -10
- package/dist/bot/task-prompt-builder.js.map +1 -1
- package/dist/bot/task-queue.d.ts +46 -0
- package/dist/bot/task-queue.d.ts.map +1 -0
- package/dist/bot/task-queue.js +237 -0
- package/dist/bot/task-queue.js.map +1 -0
- package/dist/bot/task-store.d.ts +1 -6
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +27 -78
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/task-types.d.ts +8 -4
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +2 -3
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/docs/weaver-bot-usage.md +35 -18
- package/dist/docs/docs/weaver-config.md +20 -0
- package/dist/docs/docs/weaver-task-queue.md +31 -19
- package/dist/docs/weaver-config.md +15 -9
- package/dist/mcp-tools.d.ts +17 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +98 -232
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/orchestrator-dispatch.d.ts +17 -0
- package/dist/node-types/orchestrator-dispatch.d.ts.map +1 -0
- package/dist/node-types/orchestrator-dispatch.js +63 -0
- package/dist/node-types/orchestrator-dispatch.js.map +1 -0
- package/dist/node-types/orchestrator-load-state.d.ts +16 -0
- package/dist/node-types/orchestrator-load-state.d.ts.map +1 -0
- package/dist/node-types/orchestrator-load-state.js +60 -0
- package/dist/node-types/orchestrator-load-state.js.map +1 -0
- package/dist/node-types/orchestrator-route.d.ts +16 -0
- package/dist/node-types/orchestrator-route.d.ts.map +1 -0
- package/dist/node-types/orchestrator-route.js +28 -0
- package/dist/node-types/orchestrator-route.js.map +1 -0
- package/dist/node-types/receive-task.d.ts +2 -3
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +3 -28
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/ui/bot-constants.d.ts +14 -0
- package/dist/ui/bot-constants.d.ts.map +1 -0
- package/dist/ui/bot-constants.js +189 -0
- package/dist/ui/bot-constants.js.map +1 -0
- package/dist/ui/bot-panel.js +51 -90
- package/dist/ui/bot-slot-card.js +87 -122
- package/dist/ui/budget-bar.js +5 -3
- package/dist/ui/chat-task-result.js +4 -7
- package/dist/ui/decision-log.js +136 -0
- package/dist/ui/profile-card.js +158 -0
- package/dist/ui/profile-editor.js +597 -0
- package/dist/ui/swarm-controls.js +36 -27
- package/dist/ui/swarm-dashboard.js +2034 -736
- package/dist/ui/task-create-form.js +39 -116
- package/dist/ui/task-detail-view.js +490 -239
- package/dist/ui/task-pool-list.js +69 -94
- package/dist/workflows/orchestrator.d.ts +21 -0
- package/dist/workflows/orchestrator.d.ts.map +1 -0
- package/dist/workflows/orchestrator.js +281 -0
- package/dist/workflows/orchestrator.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +253 -66
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +184 -18
- package/src/bot/ai-router.ts +132 -0
- package/src/bot/bot-registry.ts +2 -2
- package/src/bot/conversation-store.ts +2 -1
- package/src/bot/improve-loop.ts +6 -6
- package/src/bot/instance-manager.ts +128 -0
- package/src/bot/orchestrator.ts +244 -0
- package/src/bot/profile-store.ts +225 -0
- package/src/bot/profile-types.ts +141 -0
- package/src/bot/swarm-controller.ts +385 -186
- package/src/bot/task-prompt-builder.ts +37 -6
- package/src/bot/task-store.ts +28 -89
- package/src/bot/task-types.ts +10 -4
- package/src/cli-handlers.ts +2 -3
- package/src/docs/weaver-bot-usage.md +35 -18
- package/src/docs/weaver-config.md +20 -0
- package/src/docs/weaver-task-queue.md +31 -19
- package/src/mcp-tools.ts +129 -320
- package/src/node-types/orchestrator-dispatch.ts +71 -0
- package/src/node-types/orchestrator-load-state.ts +66 -0
- package/src/node-types/orchestrator-route.ts +33 -0
- package/src/node-types/receive-task.ts +3 -26
- package/src/ui/bot-constants.ts +192 -0
- package/src/ui/bot-panel.tsx +55 -79
- package/src/ui/bot-slot-card.tsx +69 -117
- package/src/ui/budget-bar.tsx +5 -3
- package/src/ui/chat-task-result.tsx +6 -9
- package/src/ui/decision-log.tsx +148 -0
- package/src/ui/profile-card.tsx +157 -0
- package/src/ui/profile-editor.tsx +384 -0
- package/src/ui/swarm-controls.tsx +35 -31
- package/src/ui/swarm-dashboard.tsx +409 -80
- package/src/ui/task-create-form.tsx +29 -119
- package/src/ui/task-detail-view.tsx +461 -215
- package/src/ui/task-pool-list.tsx +74 -95
- package/src/workflows/orchestrator.ts +302 -0
- package/dist/docs/weaver-bot-usage.md +0 -34
- package/dist/docs/weaver-genesis.md +0 -32
- package/dist/docs/weaver-task-queue.md +0 -34
- package/src/bot/error-guide.ts +0 -4
- package/src/bot/retry-utils.ts +0 -4
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SwarmController — multi-bot orchestrator for the Weaver Swarm.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
7
|
*
|
|
7
8
|
* Singleton per workspace via `SwarmController.getInstance(projectDir)`.
|
|
8
9
|
*/
|
|
@@ -15,10 +16,14 @@ import { RunStore } from './run-store.js';
|
|
|
15
16
|
import { EventLog } from './event-log.js';
|
|
16
17
|
import { runWorkflow } from './runner.js';
|
|
17
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';
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Constants
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
21
|
-
const
|
|
26
|
+
const DISPATCH_LOOP_SLEEP_MS = 2000;
|
|
22
27
|
const SWARM_STATE_FILE = 'swarm.json';
|
|
23
28
|
// ---------------------------------------------------------------------------
|
|
24
29
|
// SwarmController
|
|
@@ -30,11 +35,18 @@ export class SwarmController {
|
|
|
30
35
|
statePath;
|
|
31
36
|
taskStore;
|
|
32
37
|
eventLog;
|
|
38
|
+
orchestrator;
|
|
39
|
+
instanceManager;
|
|
40
|
+
profileStore;
|
|
33
41
|
state;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
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();
|
|
38
50
|
// -----------------------------------------------------------------------
|
|
39
51
|
// Singleton
|
|
40
52
|
// -----------------------------------------------------------------------
|
|
@@ -59,8 +71,16 @@ export class SwarmController {
|
|
|
59
71
|
this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
|
|
60
72
|
this.taskStore = new TaskStore(projectDir);
|
|
61
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);
|
|
62
77
|
// Load persisted state or create default
|
|
63
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
|
+
}
|
|
64
84
|
this._persist();
|
|
65
85
|
}
|
|
66
86
|
// -----------------------------------------------------------------------
|
|
@@ -82,27 +102,39 @@ export class SwarmController {
|
|
|
82
102
|
// Discover bots from registry
|
|
83
103
|
const registry = new BotRegistry(this.projectDir);
|
|
84
104
|
const bots = registry.list();
|
|
85
|
-
//
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
96
129
|
}
|
|
130
|
+
// Populate state fields
|
|
131
|
+
this._syncInstancesState();
|
|
97
132
|
this.state.status = 'running';
|
|
98
133
|
this.state.startedAt = new Date().toISOString();
|
|
99
134
|
this._persist();
|
|
100
135
|
this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now() });
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
for (const bot of botsToSpawn) {
|
|
104
|
-
this._spawnBotLoop(bot);
|
|
105
|
-
}
|
|
136
|
+
// Start the centralized dispatch loop
|
|
137
|
+
this._startDispatchLoop();
|
|
106
138
|
}
|
|
107
139
|
async pause() {
|
|
108
140
|
if (this.state.status !== 'running')
|
|
@@ -110,34 +142,39 @@ export class SwarmController {
|
|
|
110
142
|
this.state.status = 'paused';
|
|
111
143
|
this._persist();
|
|
112
144
|
this.eventLog.emit({ type: 'swarm-paused', timestamp: Date.now() });
|
|
113
|
-
//
|
|
145
|
+
// The dispatch loop will check paused status and skip routing
|
|
114
146
|
}
|
|
115
147
|
async stop() {
|
|
116
148
|
if (this.state.status === 'idle')
|
|
117
149
|
return;
|
|
118
150
|
this.state.status = 'stopping';
|
|
119
151
|
this._persist();
|
|
120
|
-
// Signal
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
// Signal the dispatch loop to stop
|
|
153
|
+
if (this.dispatchAbort) {
|
|
154
|
+
this.dispatchAbort.abort();
|
|
123
155
|
}
|
|
124
|
-
// Wait for
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Mark all bots as stopped
|
|
128
|
-
for (const slot of Object.values(this.state.bots)) {
|
|
129
|
-
slot.status = 'stopped';
|
|
130
|
-
slot.currentTaskId = undefined;
|
|
131
|
-
slot.currentRunId = undefined;
|
|
156
|
+
// Wait for the dispatch loop to finish
|
|
157
|
+
if (this.dispatchLoopPromise) {
|
|
158
|
+
await this.dispatchLoopPromise;
|
|
132
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();
|
|
133
167
|
this.state.status = 'idle';
|
|
134
168
|
this._persist();
|
|
135
|
-
this.
|
|
136
|
-
this.
|
|
169
|
+
this.dispatchAbort = null;
|
|
170
|
+
this.dispatchLoopPromise = null;
|
|
171
|
+
this.executionPromises.clear();
|
|
137
172
|
this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
|
|
138
173
|
}
|
|
139
174
|
getStatus() {
|
|
140
|
-
|
|
175
|
+
// Sync instances from InstanceManager before returning
|
|
176
|
+
this._syncInstancesState();
|
|
177
|
+
return { ...this.state, instances: { ...this.state.instances } };
|
|
141
178
|
}
|
|
142
179
|
configure(config) {
|
|
143
180
|
if (config.maxConcurrent !== undefined)
|
|
@@ -160,12 +197,11 @@ export class SwarmController {
|
|
|
160
197
|
// Crash recovery
|
|
161
198
|
// -----------------------------------------------------------------------
|
|
162
199
|
async recover() {
|
|
163
|
-
// Reset all
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
slot.currentRunId = undefined;
|
|
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);
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
// Reset in-progress tasks
|
|
@@ -186,18 +222,18 @@ export class SwarmController {
|
|
|
186
222
|
}
|
|
187
223
|
// Reset swarm status to idle
|
|
188
224
|
this.state.status = 'idle';
|
|
225
|
+
this._syncInstancesState();
|
|
189
226
|
this._persist();
|
|
190
227
|
}
|
|
191
228
|
// -----------------------------------------------------------------------
|
|
192
229
|
// Token/cost recording
|
|
193
230
|
// -----------------------------------------------------------------------
|
|
194
|
-
recordTokenUsage(
|
|
195
|
-
// Update
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
slot.tokensUsed += tokensUsed;
|
|
199
|
-
slot.cost += cost;
|
|
231
|
+
recordTokenUsage(instanceId, _taskId, tokensUsed, cost) {
|
|
232
|
+
// Update instance in InstanceManager
|
|
233
|
+
try {
|
|
234
|
+
this.instanceManager.recordUsage(instanceId, tokensUsed, cost);
|
|
200
235
|
}
|
|
236
|
+
catch { /* instance may not exist if stopped */ }
|
|
201
237
|
// Update session budgets
|
|
202
238
|
this.state.budgets.session.usedTokens += tokensUsed;
|
|
203
239
|
this.state.budgets.session.usedCost += cost;
|
|
@@ -211,41 +247,53 @@ export class SwarmController {
|
|
|
211
247
|
this.eventLog.emit({
|
|
212
248
|
type: 'budget-updated',
|
|
213
249
|
timestamp: Date.now(),
|
|
214
|
-
data: { botId, tokensUsed, cost },
|
|
250
|
+
data: { botId: instanceId, tokensUsed, cost },
|
|
215
251
|
});
|
|
216
252
|
}
|
|
217
253
|
// -----------------------------------------------------------------------
|
|
218
|
-
//
|
|
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
|
|
219
278
|
// -----------------------------------------------------------------------
|
|
220
|
-
|
|
279
|
+
_startDispatchLoop() {
|
|
221
280
|
const abort = new AbortController();
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
// Swallow errors in
|
|
281
|
+
this.dispatchAbort = abort;
|
|
282
|
+
this.dispatchLoopPromise = this._dispatchLoop(abort.signal).catch(() => {
|
|
283
|
+
// Swallow errors in dispatch loop — self-heals on next cycle
|
|
225
284
|
});
|
|
226
|
-
this.botLoopPromises.set(bot.id, loopPromise);
|
|
227
285
|
}
|
|
228
|
-
async
|
|
229
|
-
const botId = bot.id;
|
|
286
|
+
async _dispatchLoop(signal) {
|
|
230
287
|
while (!signal.aborted) {
|
|
231
288
|
// Check if swarm is still running
|
|
232
289
|
if (this.state.status === 'stopping' || this.state.status === 'idle')
|
|
233
290
|
break;
|
|
234
|
-
// Check if paused
|
|
291
|
+
// Check if paused — sleep and try again
|
|
235
292
|
if (this.state.status === 'paused') {
|
|
236
|
-
this.
|
|
237
|
-
await this._sleep(BOT_LOOP_SLEEP_MS, signal);
|
|
293
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
238
294
|
continue;
|
|
239
295
|
}
|
|
240
|
-
//
|
|
241
|
-
await this._checkSteering(botId);
|
|
242
|
-
// Check if this bot is individually paused
|
|
243
|
-
const slot = this.state.bots[botId];
|
|
244
|
-
if (slot?.status === 'paused') {
|
|
245
|
-
await this._sleep(BOT_LOOP_SLEEP_MS, signal);
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
// Budget check — before claiming a task
|
|
296
|
+
// Budget check before routing
|
|
249
297
|
if (this._isBudgetExceeded()) {
|
|
250
298
|
this.state.status = 'paused';
|
|
251
299
|
this._persist();
|
|
@@ -256,86 +304,131 @@ export class SwarmController {
|
|
|
256
304
|
});
|
|
257
305
|
break;
|
|
258
306
|
}
|
|
259
|
-
//
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
await this._sleep(BOT_LOOP_SLEEP_MS, signal);
|
|
264
|
-
continue;
|
|
307
|
+
// Check instance-level steering
|
|
308
|
+
const allInstances = this.instanceManager.listAll();
|
|
309
|
+
for (const inst of allInstances) {
|
|
310
|
+
await this._checkSteering(inst.instanceId);
|
|
265
311
|
}
|
|
266
|
-
//
|
|
267
|
-
this.
|
|
268
|
-
this.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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;
|
|
272
337
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
runId = RunStore.newId();
|
|
278
|
-
this._updateSlot(botId, { currentRunId: runId });
|
|
279
|
-
// Build prompt from task context
|
|
280
|
-
const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
|
|
281
|
-
const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
|
|
282
|
-
const prompt = buildTaskPrompt(task, parentTask, siblingTasks);
|
|
283
|
-
// Create per-run event log
|
|
284
|
-
const runEventLog = new EventLog(runId);
|
|
285
|
-
// Execute workflow
|
|
286
|
-
result = await runWorkflow(bot.filePath, {
|
|
287
|
-
runId,
|
|
288
|
-
taskId: task.id,
|
|
289
|
-
botId,
|
|
290
|
-
params: { taskJson: prompt, projectDir: this.projectDir },
|
|
291
|
-
eventLog: runEventLog,
|
|
292
|
-
});
|
|
293
|
-
runEventLog.done();
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
result = {
|
|
297
|
-
success: false,
|
|
298
|
-
summary: (err instanceof Error ? err.message : String(err)),
|
|
299
|
-
outcome: 'error',
|
|
300
|
-
};
|
|
301
|
-
runId = runId ?? 'error-' + Date.now();
|
|
338
|
+
if (routableTasks.length === 0) {
|
|
339
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
340
|
+
continue;
|
|
302
341
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
outcome: result.success ? 'success' : 'failed',
|
|
311
|
-
summary: result.summary || (result.success ? 'Completed' : 'Failed'),
|
|
312
|
-
filesModified: [],
|
|
313
|
-
error: result.success ? undefined : (result.summary || 'Unknown error'),
|
|
314
|
-
durationMs,
|
|
315
|
-
tokensUsed,
|
|
316
|
-
cost: costUsed,
|
|
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),
|
|
317
349
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
]);
|
|
326
416
|
}
|
|
327
417
|
else {
|
|
328
|
-
this.
|
|
418
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
329
419
|
}
|
|
330
|
-
//
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
339
432
|
if (this._isBudgetExceeded()) {
|
|
340
433
|
this.state.status = 'paused';
|
|
341
434
|
this._persist();
|
|
@@ -346,10 +439,84 @@ export class SwarmController {
|
|
|
346
439
|
});
|
|
347
440
|
break;
|
|
348
441
|
}
|
|
442
|
+
this._syncInstancesState();
|
|
349
443
|
this._persist();
|
|
350
444
|
}
|
|
351
|
-
|
|
352
|
-
|
|
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();
|
|
353
520
|
}
|
|
354
521
|
// -----------------------------------------------------------------------
|
|
355
522
|
// Budget checks
|
|
@@ -368,11 +535,22 @@ export class SwarmController {
|
|
|
368
535
|
return true;
|
|
369
536
|
return false;
|
|
370
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
|
+
}
|
|
371
549
|
// -----------------------------------------------------------------------
|
|
372
550
|
// Steering
|
|
373
551
|
// -----------------------------------------------------------------------
|
|
374
|
-
async _checkSteering(
|
|
375
|
-
const steerPath = path.join(this.weaverDir, `steer-${
|
|
552
|
+
async _checkSteering(instanceId) {
|
|
553
|
+
const steerPath = path.join(this.weaverDir, `steer-${instanceId}.json`);
|
|
376
554
|
try {
|
|
377
555
|
if (!fs.existsSync(steerPath))
|
|
378
556
|
return;
|
|
@@ -381,29 +559,27 @@ export class SwarmController {
|
|
|
381
559
|
const command = JSON.parse(raw);
|
|
382
560
|
switch (command.command) {
|
|
383
561
|
case 'pause':
|
|
384
|
-
this.
|
|
385
|
-
this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId } });
|
|
562
|
+
this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId: instanceId } });
|
|
386
563
|
break;
|
|
387
564
|
case 'resume':
|
|
388
|
-
this._updateSlot(botId, { status: 'idle' });
|
|
389
565
|
break;
|
|
390
566
|
case 'cancel':
|
|
391
|
-
|
|
392
|
-
this._updateSlot(botId, { status: 'stopped' });
|
|
567
|
+
this.instanceManager.stop(instanceId);
|
|
393
568
|
break;
|
|
394
569
|
}
|
|
395
570
|
}
|
|
396
571
|
catch { /* steer file read error — skip */ }
|
|
397
572
|
}
|
|
398
573
|
// -----------------------------------------------------------------------
|
|
399
|
-
//
|
|
574
|
+
// State sync — instances field
|
|
400
575
|
// -----------------------------------------------------------------------
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
}
|
|
407
583
|
}
|
|
408
584
|
// -----------------------------------------------------------------------
|
|
409
585
|
// State persistence
|
|
@@ -412,7 +588,10 @@ export class SwarmController {
|
|
|
412
588
|
try {
|
|
413
589
|
if (fs.existsSync(this.statePath)) {
|
|
414
590
|
const raw = fs.readFileSync(this.statePath, 'utf-8');
|
|
415
|
-
|
|
591
|
+
const data = JSON.parse(raw);
|
|
592
|
+
if (!data.instances)
|
|
593
|
+
data.instances = {};
|
|
594
|
+
return data;
|
|
416
595
|
}
|
|
417
596
|
}
|
|
418
597
|
catch { /* corrupt file — use defaults */ }
|
|
@@ -421,7 +600,7 @@ export class SwarmController {
|
|
|
421
600
|
_defaultState() {
|
|
422
601
|
return {
|
|
423
602
|
status: 'idle',
|
|
424
|
-
|
|
603
|
+
instances: {},
|
|
425
604
|
maxConcurrent: 3,
|
|
426
605
|
budgets: {
|
|
427
606
|
workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
|
|
@@ -441,7 +620,9 @@ export class SwarmController {
|
|
|
441
620
|
fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
442
621
|
fs.renameSync(tmpPath, this.statePath);
|
|
443
622
|
}
|
|
444
|
-
catch {
|
|
623
|
+
catch (err) {
|
|
624
|
+
console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
|
|
625
|
+
}
|
|
445
626
|
}
|
|
446
627
|
// -----------------------------------------------------------------------
|
|
447
628
|
// Utilities
|