@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.
- 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 +351 -335
- 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/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +49 -33
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/async-mutex.d.ts +13 -0
- package/dist/bot/async-mutex.d.ts.map +1 -0
- package/dist/bot/async-mutex.js +37 -0
- package/dist/bot/async-mutex.js.map +1 -0
- package/dist/bot/bot-manager.d.ts +2 -2
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +3 -3
- package/dist/bot/bot-manager.js.map +1 -1
- 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/dashboard.d.ts.map +1 -1
- package/dist/bot/dashboard.js +17 -8
- package/dist/bot/dashboard.js.map +1 -1
- package/dist/bot/improve-loop.js.map +1 -1
- package/dist/bot/index.d.ts +2 -4
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +1 -2
- package/dist/bot/index.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/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +8 -0
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/runner.d.ts +4 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +5 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +109 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -0
- package/dist/bot/swarm-controller.js +640 -0
- package/dist/bot/swarm-controller.js.map +1 -0
- package/dist/bot/swarm-event-log.d.ts +28 -0
- package/dist/bot/swarm-event-log.d.ts.map +1 -0
- package/dist/bot/swarm-event-log.js +54 -0
- package/dist/bot/swarm-event-log.js.map +1 -0
- package/dist/bot/task-prompt-builder.d.ts +22 -0
- package/dist/bot/task-prompt-builder.d.ts.map +1 -0
- package/dist/bot/task-prompt-builder.js +240 -0
- package/dist/bot/task-prompt-builder.js.map +1 -0
- package/dist/bot/task-store.d.ts +21 -0
- package/dist/bot/task-store.d.ts.map +1 -0
- package/dist/bot/task-store.js +364 -0
- package/dist/bot/task-store.js.map +1 -0
- package/dist/bot/task-types.d.ts +79 -0
- package/dist/bot/task-types.d.ts.map +1 -0
- package/dist/bot/task-types.js +6 -0
- package/dist/bot/task-types.js.map +1 -0
- package/dist/bot/types.d.ts +8 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +79 -54
- 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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-tools.d.ts +17 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +98 -279
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +6 -24
- package/dist/node-types/bot-report.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 -48
- 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-activity.js +2 -2
- 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 +207 -245
- package/dist/ui/bot-slot-card.js +141 -0
- package/dist/ui/budget-bar.js +59 -0
- package/dist/ui/chat-task-result.js +178 -0
- 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 +245 -0
- package/dist/ui/swarm-dashboard.js +3012 -0
- package/dist/ui/task-create-form.js +98 -0
- package/dist/ui/task-detail-view.js +1044 -0
- package/dist/ui/task-pool-list.js +156 -0
- 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 +547 -133
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +378 -371
- package/src/bot/ai-router.ts +132 -0
- package/src/bot/assistant-tools.ts +47 -29
- package/src/bot/async-mutex.ts +37 -0
- package/src/bot/bot-manager.ts +3 -3
- package/src/bot/bot-registry.ts +2 -2
- package/src/bot/conversation-store.ts +2 -1
- package/src/bot/dashboard.ts +17 -8
- package/src/bot/improve-loop.ts +6 -6
- package/src/bot/index.ts +2 -4
- 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/run-store.ts +8 -0
- package/src/bot/runner.ts +9 -1
- package/src/bot/swarm-controller.ts +780 -0
- package/src/bot/swarm-event-log.ts +57 -0
- package/src/bot/task-prompt-builder.ts +309 -0
- package/src/bot/task-store.ts +407 -0
- package/src/bot/task-types.ts +100 -0
- package/src/bot/types.ts +8 -0
- package/src/cli-handlers.ts +78 -53
- 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/index.ts +5 -4
- package/src/mcp-tools.ts +129 -372
- package/src/node-types/bot-report.ts +6 -24
- 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 -57
- package/src/ui/bot-activity.tsx +2 -2
- package/src/ui/bot-constants.ts +192 -0
- package/src/ui/bot-panel.tsx +213 -247
- package/src/ui/bot-slot-card.tsx +139 -0
- package/src/ui/budget-bar.tsx +30 -0
- package/src/ui/chat-task-result.tsx +236 -0
- 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 +260 -0
- package/src/ui/swarm-dashboard.tsx +647 -0
- package/src/ui/task-create-form.tsx +87 -0
- package/src/ui/task-detail-view.tsx +841 -0
- package/src/ui/task-pool-list.tsx +187 -0
- 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/dist/ui/bot-workspace.js +0 -1015
- package/dist/ui/chat-bot-result.js +0 -71
- package/dist/ui/queue-input.js +0 -82
- package/dist/ui/session-bar.js +0 -174
- package/src/bot/error-guide.ts +0 -4
- package/src/bot/retry-utils.ts +0 -4
- package/src/bot/session-state.ts +0 -116
- package/src/bot/task-queue.ts +0 -262
- package/src/ui/bot-workspace.tsx +0 -442
- package/src/ui/chat-bot-result.tsx +0 -81
- package/src/ui/queue-input.tsx +0 -56
- 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
|