digital-workers 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +95 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Balancing and Routing for Agent Coordination
|
|
3
|
+
*
|
|
4
|
+
* Provides intelligent task distribution and priority-based handling for
|
|
5
|
+
* coordinating work across multiple agents. Includes multiple balancing
|
|
6
|
+
* strategies, capability-based routing, and comprehensive metrics.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Safe Array Access Utilities
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Safely get the first element of an array.
|
|
15
|
+
*
|
|
16
|
+
* Provides type-safe access to array elements without non-null assertions.
|
|
17
|
+
*
|
|
18
|
+
* @param arr - The array to access
|
|
19
|
+
* @returns The first element or undefined if array is empty
|
|
20
|
+
*/
|
|
21
|
+
function safeFirst(arr) {
|
|
22
|
+
return arr.length > 0 ? arr[0] : undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Safely get an element at a specific index.
|
|
26
|
+
*
|
|
27
|
+
* Provides type-safe access with bounds checking.
|
|
28
|
+
*
|
|
29
|
+
* @param arr - The array to access
|
|
30
|
+
* @param index - The index to access
|
|
31
|
+
* @returns The element at the index or undefined if out of bounds
|
|
32
|
+
*/
|
|
33
|
+
function safeAt(arr, index) {
|
|
34
|
+
if (arr.length === 0 || index < 0 || index >= arr.length) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return arr[index];
|
|
38
|
+
}
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// MetricsCollector Implementation
|
|
41
|
+
// ============================================================================
|
|
42
|
+
/**
|
|
43
|
+
* Create a new MetricsCollector instance with isolated state.
|
|
44
|
+
*
|
|
45
|
+
* This factory function creates a thread-safe metrics collector that
|
|
46
|
+
* encapsulates all metrics state within the returned instance. Multiple
|
|
47
|
+
* collectors can be used independently without interference.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* // Create isolated collectors for different environments
|
|
52
|
+
* const prodCollector = createMetricsCollector()
|
|
53
|
+
* const testCollector = createMetricsCollector()
|
|
54
|
+
*
|
|
55
|
+
* // Balancers with separate metrics
|
|
56
|
+
* const prodBalancer = createRoundRobinBalancer(agents, { metricsCollector: prodCollector })
|
|
57
|
+
* const testBalancer = createRoundRobinBalancer(agents, { metricsCollector: testCollector })
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Shared collector for aggregated metrics
|
|
63
|
+
* const sharedCollector = createMetricsCollector()
|
|
64
|
+
* const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
65
|
+
* const balancer2 = createLeastBusyBalancer(agents, { metricsCollector: sharedCollector })
|
|
66
|
+
*
|
|
67
|
+
* // Get combined metrics
|
|
68
|
+
* const metrics = sharedCollector.collect()
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @returns A new MetricsCollector instance
|
|
72
|
+
*/
|
|
73
|
+
export function createMetricsCollector() {
|
|
74
|
+
// Instance-local state - each collector has its own isolated metrics
|
|
75
|
+
let metricsState = {
|
|
76
|
+
totalRouted: 0,
|
|
77
|
+
failedRoutes: 0,
|
|
78
|
+
averageLatencyMs: 0,
|
|
79
|
+
perAgent: {},
|
|
80
|
+
strategyUsage: {},
|
|
81
|
+
};
|
|
82
|
+
let totalLatency = 0;
|
|
83
|
+
function record(result, latencyMs, strategy) {
|
|
84
|
+
metricsState.totalRouted++;
|
|
85
|
+
totalLatency += latencyMs;
|
|
86
|
+
metricsState.averageLatencyMs = totalLatency / metricsState.totalRouted;
|
|
87
|
+
if (!result.agent) {
|
|
88
|
+
metricsState.failedRoutes++;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const agentId = result.agent.id;
|
|
92
|
+
if (!metricsState.perAgent[agentId]) {
|
|
93
|
+
metricsState.perAgent[agentId] = { routedCount: 0 };
|
|
94
|
+
}
|
|
95
|
+
metricsState.perAgent[agentId].routedCount++;
|
|
96
|
+
metricsState.perAgent[agentId].lastRouted = new Date();
|
|
97
|
+
}
|
|
98
|
+
metricsState.strategyUsage[strategy] = (metricsState.strategyUsage[strategy] || 0) + 1;
|
|
99
|
+
}
|
|
100
|
+
function collect() {
|
|
101
|
+
// Return a deep copy to prevent external mutation
|
|
102
|
+
return {
|
|
103
|
+
...metricsState,
|
|
104
|
+
perAgent: { ...metricsState.perAgent },
|
|
105
|
+
strategyUsage: { ...metricsState.strategyUsage },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function reset() {
|
|
109
|
+
metricsState = {
|
|
110
|
+
totalRouted: 0,
|
|
111
|
+
failedRoutes: 0,
|
|
112
|
+
averageLatencyMs: 0,
|
|
113
|
+
perAgent: {},
|
|
114
|
+
strategyUsage: {},
|
|
115
|
+
};
|
|
116
|
+
totalLatency = 0;
|
|
117
|
+
}
|
|
118
|
+
return { record, collect, reset };
|
|
119
|
+
}
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Default Global Metrics Collector (Backward Compatibility)
|
|
122
|
+
// ============================================================================
|
|
123
|
+
/**
|
|
124
|
+
* Default metrics collector singleton for backward compatibility.
|
|
125
|
+
* Used when no explicit collector is provided to balancer factories.
|
|
126
|
+
*/
|
|
127
|
+
const defaultMetricsCollector = createMetricsCollector();
|
|
128
|
+
/**
|
|
129
|
+
* Collect current routing metrics from the default global collector.
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* This function is provided for backward compatibility. For new code,
|
|
133
|
+
* consider using explicit MetricsCollector instances for better isolation.
|
|
134
|
+
*
|
|
135
|
+
* @returns Current routing metrics
|
|
136
|
+
*/
|
|
137
|
+
export function collectRoutingMetrics() {
|
|
138
|
+
return defaultMetricsCollector.collect();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Reset all routing metrics in the default global collector.
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* This function is provided for backward compatibility. For new code,
|
|
145
|
+
* consider using explicit MetricsCollector instances for better isolation.
|
|
146
|
+
*/
|
|
147
|
+
export function resetRoutingMetrics() {
|
|
148
|
+
defaultMetricsCollector.reset();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a round-robin load balancer
|
|
152
|
+
*
|
|
153
|
+
* Distributes tasks evenly across all available agents in order.
|
|
154
|
+
*
|
|
155
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
156
|
+
* @param options - Optional configuration including metricsCollector
|
|
157
|
+
* @returns A LoadBalancer instance
|
|
158
|
+
*/
|
|
159
|
+
export function createRoundRobinBalancer(initialAgents, options = {}) {
|
|
160
|
+
let agents = [...initialAgents];
|
|
161
|
+
let currentIndex = 0;
|
|
162
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector;
|
|
163
|
+
function getAvailableAgents() {
|
|
164
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy');
|
|
165
|
+
}
|
|
166
|
+
function route(task) {
|
|
167
|
+
const start = performance.now();
|
|
168
|
+
const available = getAvailableAgents();
|
|
169
|
+
if (available.length === 0) {
|
|
170
|
+
const result = {
|
|
171
|
+
agent: null,
|
|
172
|
+
task,
|
|
173
|
+
strategy: 'round-robin',
|
|
174
|
+
timestamp: new Date(),
|
|
175
|
+
reason: 'no-available-agents',
|
|
176
|
+
};
|
|
177
|
+
collector.record(result, performance.now() - start, 'round-robin');
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
// Find the next available agent starting from current index
|
|
181
|
+
let attempts = 0;
|
|
182
|
+
while (attempts < agents.length) {
|
|
183
|
+
const agent = safeAt(agents, currentIndex % agents.length);
|
|
184
|
+
currentIndex++;
|
|
185
|
+
if (!agent) {
|
|
186
|
+
attempts++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (agent.status === 'available' || agent.status === 'busy') {
|
|
190
|
+
const result = {
|
|
191
|
+
agent,
|
|
192
|
+
task,
|
|
193
|
+
strategy: 'round-robin',
|
|
194
|
+
timestamp: new Date(),
|
|
195
|
+
};
|
|
196
|
+
collector.record(result, performance.now() - start, 'round-robin');
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
attempts++;
|
|
200
|
+
}
|
|
201
|
+
const result = {
|
|
202
|
+
agent: null,
|
|
203
|
+
task,
|
|
204
|
+
strategy: 'round-robin',
|
|
205
|
+
timestamp: new Date(),
|
|
206
|
+
reason: 'no-available-agents',
|
|
207
|
+
};
|
|
208
|
+
collector.record(result, performance.now() - start, 'round-robin');
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
function addAgent(agent) {
|
|
212
|
+
agents.push(agent);
|
|
213
|
+
}
|
|
214
|
+
function removeAgent(agentId) {
|
|
215
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
216
|
+
}
|
|
217
|
+
function getAgents() {
|
|
218
|
+
return [...agents];
|
|
219
|
+
}
|
|
220
|
+
return { route, addAgent, removeAgent, getAgents };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Create a least-busy load balancer
|
|
224
|
+
*
|
|
225
|
+
* Routes tasks to agents with the lowest current load.
|
|
226
|
+
*
|
|
227
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
228
|
+
* @param options - Optional configuration including metricsCollector
|
|
229
|
+
* @returns A LeastBusyBalancer instance
|
|
230
|
+
*/
|
|
231
|
+
export function createLeastBusyBalancer(initialAgents, options = {}) {
|
|
232
|
+
let agents = [...initialAgents];
|
|
233
|
+
const loadTracking = new Map();
|
|
234
|
+
let lastRoutedIndex = -1;
|
|
235
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector;
|
|
236
|
+
// Initialize load tracking
|
|
237
|
+
agents.forEach((a) => loadTracking.set(a.id, a.currentLoad));
|
|
238
|
+
function getAvailableAgents() {
|
|
239
|
+
return agents.filter((a) => {
|
|
240
|
+
if (a.status !== 'available' && a.status !== 'busy')
|
|
241
|
+
return false;
|
|
242
|
+
const load = loadTracking.get(a.id) ?? a.currentLoad;
|
|
243
|
+
return load < a.maxLoad;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function route(task) {
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
const available = getAvailableAgents();
|
|
249
|
+
if (available.length === 0) {
|
|
250
|
+
const result = {
|
|
251
|
+
agent: null,
|
|
252
|
+
task,
|
|
253
|
+
strategy: 'least-busy',
|
|
254
|
+
timestamp: new Date(),
|
|
255
|
+
reason: 'no-available-agents',
|
|
256
|
+
};
|
|
257
|
+
collector.record(result, performance.now() - start, 'least-busy');
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
// Sort by load percentage
|
|
261
|
+
const sorted = [...available].sort((a, b) => {
|
|
262
|
+
const loadA = (loadTracking.get(a.id) ?? a.currentLoad) / a.maxLoad;
|
|
263
|
+
const loadB = (loadTracking.get(b.id) ?? b.currentLoad) / b.maxLoad;
|
|
264
|
+
if (loadA === loadB) {
|
|
265
|
+
// Tie-breaking with round-robin behavior
|
|
266
|
+
const indexA = agents.indexOf(a);
|
|
267
|
+
const indexB = agents.indexOf(b);
|
|
268
|
+
const distA = (indexA - lastRoutedIndex + agents.length) % agents.length;
|
|
269
|
+
const distB = (indexB - lastRoutedIndex + agents.length) % agents.length;
|
|
270
|
+
return distA - distB;
|
|
271
|
+
}
|
|
272
|
+
return loadA - loadB;
|
|
273
|
+
});
|
|
274
|
+
const selected = safeFirst(sorted);
|
|
275
|
+
if (!selected) {
|
|
276
|
+
const result = {
|
|
277
|
+
agent: null,
|
|
278
|
+
task,
|
|
279
|
+
strategy: 'least-busy',
|
|
280
|
+
timestamp: new Date(),
|
|
281
|
+
reason: 'no-available-agents',
|
|
282
|
+
};
|
|
283
|
+
collector.record(result, performance.now() - start, 'least-busy');
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
lastRoutedIndex = agents.indexOf(selected);
|
|
287
|
+
// Increment load
|
|
288
|
+
const currentLoad = loadTracking.get(selected.id) ?? selected.currentLoad;
|
|
289
|
+
loadTracking.set(selected.id, currentLoad + 1);
|
|
290
|
+
const result = {
|
|
291
|
+
agent: selected,
|
|
292
|
+
task,
|
|
293
|
+
strategy: 'least-busy',
|
|
294
|
+
timestamp: new Date(),
|
|
295
|
+
};
|
|
296
|
+
collector.record(result, performance.now() - start, 'least-busy');
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
function addAgent(agent) {
|
|
300
|
+
agents.push(agent);
|
|
301
|
+
loadTracking.set(agent.id, agent.currentLoad);
|
|
302
|
+
}
|
|
303
|
+
function removeAgent(agentId) {
|
|
304
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
305
|
+
loadTracking.delete(agentId);
|
|
306
|
+
}
|
|
307
|
+
function getAgents() {
|
|
308
|
+
return [...agents];
|
|
309
|
+
}
|
|
310
|
+
function getLoadMetrics() {
|
|
311
|
+
const metrics = {};
|
|
312
|
+
agents.forEach((a) => {
|
|
313
|
+
const load = loadTracking.get(a.id) ?? a.currentLoad;
|
|
314
|
+
metrics[a.id] = load / a.maxLoad;
|
|
315
|
+
});
|
|
316
|
+
return metrics;
|
|
317
|
+
}
|
|
318
|
+
function releaseLoad(agentId) {
|
|
319
|
+
const current = loadTracking.get(agentId);
|
|
320
|
+
if (current !== undefined && current > 0) {
|
|
321
|
+
loadTracking.set(agentId, current - 1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function setLoad(agentId, load) {
|
|
325
|
+
loadTracking.set(agentId, load);
|
|
326
|
+
}
|
|
327
|
+
return { route, addAgent, removeAgent, getAgents, getLoadMetrics, releaseLoad, setLoad };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Create a capability-based router
|
|
331
|
+
*
|
|
332
|
+
* Routes tasks to agents that have the required skills.
|
|
333
|
+
*
|
|
334
|
+
* @param initialAgents - Initial set of agents to route to
|
|
335
|
+
* @param options - Optional configuration including metricsCollector
|
|
336
|
+
* @returns A CapabilityRouter instance
|
|
337
|
+
*/
|
|
338
|
+
export function createCapabilityRouter(initialAgents, options = {}) {
|
|
339
|
+
let agents = [...initialAgents];
|
|
340
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector;
|
|
341
|
+
function getAvailableAgents() {
|
|
342
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy');
|
|
343
|
+
}
|
|
344
|
+
function hasAllSkills(agent, requiredSkills) {
|
|
345
|
+
return requiredSkills.every((skill) => agent.skills.includes(skill));
|
|
346
|
+
}
|
|
347
|
+
function calculateMatchScore(agent, requiredSkills) {
|
|
348
|
+
if (requiredSkills.length === 0)
|
|
349
|
+
return 1;
|
|
350
|
+
const matchingSkills = requiredSkills.filter((s) => agent.skills.includes(s));
|
|
351
|
+
return matchingSkills.length / requiredSkills.length;
|
|
352
|
+
}
|
|
353
|
+
function route(task) {
|
|
354
|
+
const start = performance.now();
|
|
355
|
+
const available = getAvailableAgents();
|
|
356
|
+
// Find agents with all required skills
|
|
357
|
+
let candidates = available.filter((a) => hasAllSkills(a, task.requiredSkills));
|
|
358
|
+
if (candidates.length === 0) {
|
|
359
|
+
const result = {
|
|
360
|
+
agent: null,
|
|
361
|
+
task,
|
|
362
|
+
strategy: 'capability',
|
|
363
|
+
timestamp: new Date(),
|
|
364
|
+
reason: 'no-matching-capability',
|
|
365
|
+
};
|
|
366
|
+
collector.record(result, performance.now() - start, 'capability');
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
// Sort by match quality
|
|
370
|
+
if (options.preferExactMatch) {
|
|
371
|
+
// Prefer agents with closest skill count to requirements
|
|
372
|
+
candidates.sort((a, b) => {
|
|
373
|
+
const diffA = Math.abs(a.skills.length - task.requiredSkills.length);
|
|
374
|
+
const diffB = Math.abs(b.skills.length - task.requiredSkills.length);
|
|
375
|
+
return diffA - diffB;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const selected = safeFirst(candidates);
|
|
379
|
+
if (!selected) {
|
|
380
|
+
const result = {
|
|
381
|
+
agent: null,
|
|
382
|
+
task,
|
|
383
|
+
strategy: 'capability',
|
|
384
|
+
timestamp: new Date(),
|
|
385
|
+
reason: 'no-matching-capability',
|
|
386
|
+
};
|
|
387
|
+
collector.record(result, performance.now() - start, 'capability');
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
const matchScore = calculateMatchScore(selected, task.requiredSkills);
|
|
391
|
+
const result = {
|
|
392
|
+
agent: selected,
|
|
393
|
+
task,
|
|
394
|
+
strategy: 'capability',
|
|
395
|
+
timestamp: new Date(),
|
|
396
|
+
matchScore,
|
|
397
|
+
};
|
|
398
|
+
collector.record(result, performance.now() - start, 'capability');
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
function addAgent(agent) {
|
|
402
|
+
agents.push(agent);
|
|
403
|
+
}
|
|
404
|
+
function removeAgent(agentId) {
|
|
405
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
406
|
+
}
|
|
407
|
+
function getAgents() {
|
|
408
|
+
return [...agents];
|
|
409
|
+
}
|
|
410
|
+
function findAgentsWithSkills(skills) {
|
|
411
|
+
return agents.filter((a) => hasAllSkills(a, skills));
|
|
412
|
+
}
|
|
413
|
+
function getSkillCoverage() {
|
|
414
|
+
const coverage = {};
|
|
415
|
+
agents.forEach((a) => {
|
|
416
|
+
a.skills.forEach((skill) => {
|
|
417
|
+
coverage[skill] = (coverage[skill] || 0) + 1;
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
return coverage;
|
|
421
|
+
}
|
|
422
|
+
return { route, addAgent, removeAgent, getAgents, findAgentsWithSkills, getSkillCoverage };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Create a priority queue balancer
|
|
426
|
+
*
|
|
427
|
+
* Processes tasks in priority order with optional aging to prevent starvation.
|
|
428
|
+
*
|
|
429
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
430
|
+
* @param options - Optional configuration including metricsCollector
|
|
431
|
+
* @returns A PriorityQueueBalancer instance
|
|
432
|
+
*/
|
|
433
|
+
export function createPriorityQueueBalancer(initialAgents, options = {}) {
|
|
434
|
+
let agents = [...initialAgents];
|
|
435
|
+
const queue = [];
|
|
436
|
+
const { enableAging = false, agingBoostPerSecond = 1, maxWaitTime } = options;
|
|
437
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector;
|
|
438
|
+
function getAvailableAgents() {
|
|
439
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy');
|
|
440
|
+
}
|
|
441
|
+
function getEffectivePriority(taskId) {
|
|
442
|
+
const task = queue.find((t) => t.id === taskId);
|
|
443
|
+
if (!task)
|
|
444
|
+
return 0;
|
|
445
|
+
let priority = task.priority;
|
|
446
|
+
if (enableAging && task.enqueuedAt) {
|
|
447
|
+
const waitTimeMs = Date.now() - task.enqueuedAt.getTime();
|
|
448
|
+
const waitTimeSeconds = waitTimeMs / 1000;
|
|
449
|
+
priority += waitTimeSeconds * agingBoostPerSecond;
|
|
450
|
+
}
|
|
451
|
+
if (maxWaitTime && task.enqueuedAt) {
|
|
452
|
+
const waitTimeMs = Date.now() - task.enqueuedAt.getTime();
|
|
453
|
+
if (waitTimeMs >= maxWaitTime) {
|
|
454
|
+
priority = Infinity; // Promote to highest priority
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return priority;
|
|
458
|
+
}
|
|
459
|
+
function sortQueue() {
|
|
460
|
+
queue.sort((a, b) => {
|
|
461
|
+
const priorityA = getEffectivePriority(a.id);
|
|
462
|
+
const priorityB = getEffectivePriority(b.id);
|
|
463
|
+
if (priorityB !== priorityA) {
|
|
464
|
+
return priorityB - priorityA; // Higher priority first
|
|
465
|
+
}
|
|
466
|
+
// FIFO for equal priority
|
|
467
|
+
const timeA = a.enqueuedAt?.getTime() ?? 0;
|
|
468
|
+
const timeB = b.enqueuedAt?.getTime() ?? 0;
|
|
469
|
+
return timeA - timeB;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function enqueue(task) {
|
|
473
|
+
if (task.priority < 1 || task.priority > 10) {
|
|
474
|
+
throw new Error('Priority must be between 1 and 10');
|
|
475
|
+
}
|
|
476
|
+
const taskWithTime = { ...task, enqueuedAt: new Date() };
|
|
477
|
+
queue.push(taskWithTime);
|
|
478
|
+
sortQueue();
|
|
479
|
+
}
|
|
480
|
+
async function routeNext() {
|
|
481
|
+
if (queue.length === 0)
|
|
482
|
+
return null;
|
|
483
|
+
sortQueue();
|
|
484
|
+
const task = queue.shift();
|
|
485
|
+
if (!task)
|
|
486
|
+
return null;
|
|
487
|
+
const start = performance.now();
|
|
488
|
+
const available = getAvailableAgents();
|
|
489
|
+
if (available.length === 0) {
|
|
490
|
+
const result = {
|
|
491
|
+
agent: null,
|
|
492
|
+
task,
|
|
493
|
+
strategy: 'priority-queue',
|
|
494
|
+
timestamp: new Date(),
|
|
495
|
+
reason: 'no-available-agents',
|
|
496
|
+
};
|
|
497
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
// Simple round-robin among available for now
|
|
501
|
+
const agent = safeFirst(available);
|
|
502
|
+
if (!agent) {
|
|
503
|
+
const result = {
|
|
504
|
+
agent: null,
|
|
505
|
+
task,
|
|
506
|
+
strategy: 'priority-queue',
|
|
507
|
+
timestamp: new Date(),
|
|
508
|
+
reason: 'no-available-agents',
|
|
509
|
+
};
|
|
510
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
const result = {
|
|
514
|
+
agent,
|
|
515
|
+
task,
|
|
516
|
+
strategy: 'priority-queue',
|
|
517
|
+
timestamp: new Date(),
|
|
518
|
+
};
|
|
519
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
function route(task) {
|
|
523
|
+
const start = performance.now();
|
|
524
|
+
const available = getAvailableAgents();
|
|
525
|
+
if (available.length === 0) {
|
|
526
|
+
const result = {
|
|
527
|
+
agent: null,
|
|
528
|
+
task,
|
|
529
|
+
strategy: 'priority-queue',
|
|
530
|
+
timestamp: new Date(),
|
|
531
|
+
reason: 'no-available-agents',
|
|
532
|
+
};
|
|
533
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
const agent = safeFirst(available);
|
|
537
|
+
if (!agent) {
|
|
538
|
+
const result = {
|
|
539
|
+
agent: null,
|
|
540
|
+
task,
|
|
541
|
+
strategy: 'priority-queue',
|
|
542
|
+
timestamp: new Date(),
|
|
543
|
+
reason: 'no-available-agents',
|
|
544
|
+
};
|
|
545
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
const result = {
|
|
549
|
+
agent,
|
|
550
|
+
task,
|
|
551
|
+
strategy: 'priority-queue',
|
|
552
|
+
timestamp: new Date(),
|
|
553
|
+
};
|
|
554
|
+
collector.record(result, performance.now() - start, 'priority-queue');
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
function addAgent(agent) {
|
|
558
|
+
agents.push(agent);
|
|
559
|
+
}
|
|
560
|
+
function removeAgent(agentId) {
|
|
561
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
562
|
+
}
|
|
563
|
+
function getAgents() {
|
|
564
|
+
return [...agents];
|
|
565
|
+
}
|
|
566
|
+
function queueSize() {
|
|
567
|
+
return queue.length;
|
|
568
|
+
}
|
|
569
|
+
function clear() {
|
|
570
|
+
queue.length = 0;
|
|
571
|
+
}
|
|
572
|
+
function peek() {
|
|
573
|
+
if (queue.length === 0)
|
|
574
|
+
return null;
|
|
575
|
+
sortQueue();
|
|
576
|
+
return queue[0] ?? null;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
route,
|
|
580
|
+
addAgent,
|
|
581
|
+
removeAgent,
|
|
582
|
+
getAgents,
|
|
583
|
+
enqueue,
|
|
584
|
+
routeNext,
|
|
585
|
+
queueSize,
|
|
586
|
+
clear,
|
|
587
|
+
peek,
|
|
588
|
+
getEffectivePriority,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Create an agent availability tracker
|
|
593
|
+
*
|
|
594
|
+
* Tracks agent status, heartbeats, and capacity.
|
|
595
|
+
*/
|
|
596
|
+
export function createAgentAvailabilityTracker(initialAgents, options = {}) {
|
|
597
|
+
const agents = new Map();
|
|
598
|
+
const availability = new Map();
|
|
599
|
+
const handlers = [];
|
|
600
|
+
const { heartbeatTimeout = 30000 } = options;
|
|
601
|
+
// Initialize
|
|
602
|
+
initialAgents.forEach((a) => {
|
|
603
|
+
agents.set(a.id, a);
|
|
604
|
+
availability.set(a.id, {
|
|
605
|
+
status: a.status,
|
|
606
|
+
lastSeen: new Date(),
|
|
607
|
+
currentLoad: a.currentLoad,
|
|
608
|
+
maxLoad: a.maxLoad,
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
function getAvailability(agentId) {
|
|
612
|
+
return (availability.get(agentId) ?? {
|
|
613
|
+
status: 'offline',
|
|
614
|
+
lastSeen: new Date(0),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function updateStatus(agentId, status) {
|
|
618
|
+
const current = availability.get(agentId);
|
|
619
|
+
const previousStatus = current?.status ?? 'offline';
|
|
620
|
+
availability.set(agentId, {
|
|
621
|
+
...current,
|
|
622
|
+
status,
|
|
623
|
+
lastSeen: new Date(),
|
|
624
|
+
});
|
|
625
|
+
const agent = agents.get(agentId);
|
|
626
|
+
if (agent) {
|
|
627
|
+
agent.status = status;
|
|
628
|
+
}
|
|
629
|
+
// Emit status change event
|
|
630
|
+
if (previousStatus !== status) {
|
|
631
|
+
const event = {
|
|
632
|
+
agentId,
|
|
633
|
+
previousStatus,
|
|
634
|
+
currentStatus: status,
|
|
635
|
+
timestamp: new Date(),
|
|
636
|
+
};
|
|
637
|
+
handlers.forEach((h) => h(event));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function getAvailableAgents() {
|
|
641
|
+
return Array.from(agents.values()).filter((a) => {
|
|
642
|
+
const avail = availability.get(a.id);
|
|
643
|
+
return avail?.status === 'available' || avail?.status === 'busy';
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function heartbeat(agentId) {
|
|
647
|
+
const current = availability.get(agentId);
|
|
648
|
+
if (current) {
|
|
649
|
+
availability.set(agentId, {
|
|
650
|
+
...current,
|
|
651
|
+
lastSeen: new Date(),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function checkTimeouts() {
|
|
656
|
+
const now = Date.now();
|
|
657
|
+
availability.forEach((avail, agentId) => {
|
|
658
|
+
if (avail.status !== 'offline') {
|
|
659
|
+
const timeSinceLastSeen = now - avail.lastSeen.getTime();
|
|
660
|
+
if (timeSinceLastSeen > heartbeatTimeout) {
|
|
661
|
+
updateStatus(agentId, 'offline');
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function onStatusChange(handler) {
|
|
667
|
+
handlers.push(handler);
|
|
668
|
+
}
|
|
669
|
+
function updateLoad(agentId, current, max) {
|
|
670
|
+
const avail = availability.get(agentId);
|
|
671
|
+
if (avail) {
|
|
672
|
+
availability.set(agentId, {
|
|
673
|
+
...avail,
|
|
674
|
+
currentLoad: current,
|
|
675
|
+
maxLoad: max,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function getCapacityUtilization() {
|
|
680
|
+
const result = {};
|
|
681
|
+
availability.forEach((avail, agentId) => {
|
|
682
|
+
if (avail.currentLoad !== undefined && avail.maxLoad !== undefined && avail.maxLoad > 0) {
|
|
683
|
+
result[agentId] = avail.currentLoad / avail.maxLoad;
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
function getOverallCapacity() {
|
|
689
|
+
let total = 0;
|
|
690
|
+
let used = 0;
|
|
691
|
+
availability.forEach((avail, agentId) => {
|
|
692
|
+
const agent = agents.get(agentId);
|
|
693
|
+
if (agent && (avail.status === 'available' || avail.status === 'busy')) {
|
|
694
|
+
total += avail.maxLoad ?? agent.maxLoad;
|
|
695
|
+
used += avail.currentLoad ?? agent.currentLoad;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
total,
|
|
700
|
+
used,
|
|
701
|
+
available: total - used,
|
|
702
|
+
utilization: total > 0 ? used / total : 0,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
getAvailability,
|
|
707
|
+
updateStatus,
|
|
708
|
+
getAvailableAgents,
|
|
709
|
+
heartbeat,
|
|
710
|
+
checkTimeouts,
|
|
711
|
+
onStatusChange,
|
|
712
|
+
updateLoad,
|
|
713
|
+
getCapacityUtilization,
|
|
714
|
+
getOverallCapacity,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Create a routing rule engine
|
|
719
|
+
*
|
|
720
|
+
* Evaluates routing rules in priority order to determine task routing.
|
|
721
|
+
*
|
|
722
|
+
* @param initialAgents - Initial set of agents to route to
|
|
723
|
+
* @param options - Optional configuration including metricsCollector
|
|
724
|
+
* @returns A RoutingRuleEngine instance
|
|
725
|
+
*/
|
|
726
|
+
export function createRoutingRuleEngine(initialAgents, options = {}) {
|
|
727
|
+
let agents = [...initialAgents];
|
|
728
|
+
const rules = [];
|
|
729
|
+
const { defaultStrategy = 'round-robin' } = options;
|
|
730
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector;
|
|
731
|
+
// Create default balancer for fallback
|
|
732
|
+
let defaultBalancer;
|
|
733
|
+
function getDefaultBalancer() {
|
|
734
|
+
if (!defaultBalancer) {
|
|
735
|
+
const balancerOptions = { metricsCollector: collector };
|
|
736
|
+
switch (defaultStrategy) {
|
|
737
|
+
case 'least-busy':
|
|
738
|
+
defaultBalancer = createLeastBusyBalancer(agents, balancerOptions);
|
|
739
|
+
break;
|
|
740
|
+
case 'capability':
|
|
741
|
+
defaultBalancer = createCapabilityRouter(agents, balancerOptions);
|
|
742
|
+
break;
|
|
743
|
+
default:
|
|
744
|
+
defaultBalancer = createRoundRobinBalancer(agents, balancerOptions);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return defaultBalancer;
|
|
748
|
+
}
|
|
749
|
+
function evaluateCondition(condition, task) {
|
|
750
|
+
if (typeof condition === 'function') {
|
|
751
|
+
return condition(task);
|
|
752
|
+
}
|
|
753
|
+
// Declarative condition evaluation
|
|
754
|
+
if (condition.requiredSkills?.contains) {
|
|
755
|
+
if (!task.requiredSkills.includes(condition.requiredSkills.contains)) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (condition.priority) {
|
|
760
|
+
if (condition.priority.gte !== undefined && task.priority < condition.priority.gte) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
if (condition.priority.lte !== undefined && task.priority > condition.priority.lte) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (condition.metadata) {
|
|
768
|
+
for (const [key, value] of Object.entries(condition.metadata)) {
|
|
769
|
+
if (task.metadata[key] !== value) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
function route(task) {
|
|
777
|
+
const start = performance.now();
|
|
778
|
+
// Sort rules by priority (descending)
|
|
779
|
+
const sortedRules = [...rules]
|
|
780
|
+
.filter((r) => r.enabled !== false)
|
|
781
|
+
.sort((a, b) => b.priority - a.priority);
|
|
782
|
+
// Evaluate rules
|
|
783
|
+
for (const rule of sortedRules) {
|
|
784
|
+
if (evaluateCondition(rule.condition, task)) {
|
|
785
|
+
const agent = rule.action(task, agents);
|
|
786
|
+
if (agent) {
|
|
787
|
+
const result = {
|
|
788
|
+
agent,
|
|
789
|
+
task,
|
|
790
|
+
strategy: 'custom',
|
|
791
|
+
timestamp: new Date(),
|
|
792
|
+
matchedRule: rule.name,
|
|
793
|
+
};
|
|
794
|
+
collector.record(result, performance.now() - start, 'custom');
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Use default strategy as fallback
|
|
800
|
+
const defaultResult = getDefaultBalancer().route(task);
|
|
801
|
+
return {
|
|
802
|
+
...defaultResult,
|
|
803
|
+
matchedRule: null,
|
|
804
|
+
usedDefault: true,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function addRule(rule) {
|
|
808
|
+
if (!rule.name || rule.name.trim() === '') {
|
|
809
|
+
throw new Error('Rule name is required');
|
|
810
|
+
}
|
|
811
|
+
if (rule.priority < 0) {
|
|
812
|
+
throw new Error('Rule priority must be non-negative');
|
|
813
|
+
}
|
|
814
|
+
rules.push({ ...rule, enabled: rule.enabled ?? true });
|
|
815
|
+
}
|
|
816
|
+
function removeRule(name) {
|
|
817
|
+
const index = rules.findIndex((r) => r.name === name);
|
|
818
|
+
if (index !== -1) {
|
|
819
|
+
rules.splice(index, 1);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function updateRule(name, updates) {
|
|
823
|
+
const rule = rules.find((r) => r.name === name);
|
|
824
|
+
if (rule) {
|
|
825
|
+
Object.assign(rule, updates);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function enableRule(name) {
|
|
829
|
+
const rule = rules.find((r) => r.name === name);
|
|
830
|
+
if (rule) {
|
|
831
|
+
rule.enabled = true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function disableRule(name) {
|
|
835
|
+
const rule = rules.find((r) => r.name === name);
|
|
836
|
+
if (rule) {
|
|
837
|
+
rule.enabled = false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function getRules() {
|
|
841
|
+
return [...rules];
|
|
842
|
+
}
|
|
843
|
+
function addAgent(agent) {
|
|
844
|
+
agents.push(agent);
|
|
845
|
+
defaultBalancer = undefined; // Reset default balancer
|
|
846
|
+
}
|
|
847
|
+
function removeAgent(agentId) {
|
|
848
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
849
|
+
defaultBalancer = undefined; // Reset default balancer
|
|
850
|
+
}
|
|
851
|
+
function getAgents() {
|
|
852
|
+
return [...agents];
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
route,
|
|
856
|
+
addAgent,
|
|
857
|
+
removeAgent,
|
|
858
|
+
getAgents,
|
|
859
|
+
addRule,
|
|
860
|
+
removeRule,
|
|
861
|
+
updateRule,
|
|
862
|
+
enableRule,
|
|
863
|
+
disableRule,
|
|
864
|
+
getRules,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Create a composite load balancer
|
|
869
|
+
*
|
|
870
|
+
* Combines multiple balancing strategies for sophisticated routing decisions.
|
|
871
|
+
*
|
|
872
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
873
|
+
* @param config - Configuration including strategies and optional metricsCollector
|
|
874
|
+
* @returns A CompositeBalancer instance
|
|
875
|
+
*/
|
|
876
|
+
export function createCompositeBalancer(initialAgents, config) {
|
|
877
|
+
let agents = [...initialAgents];
|
|
878
|
+
const balancers = new Map();
|
|
879
|
+
const collector = config.metricsCollector ?? defaultMetricsCollector;
|
|
880
|
+
// Initialize balancers
|
|
881
|
+
function getOrCreateBalancer(strategy) {
|
|
882
|
+
if (!balancers.has(strategy)) {
|
|
883
|
+
const balancerOptions = { metricsCollector: collector };
|
|
884
|
+
switch (strategy) {
|
|
885
|
+
case 'round-robin':
|
|
886
|
+
balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions));
|
|
887
|
+
break;
|
|
888
|
+
case 'least-busy':
|
|
889
|
+
balancers.set(strategy, createLeastBusyBalancer(agents, balancerOptions));
|
|
890
|
+
break;
|
|
891
|
+
case 'capability':
|
|
892
|
+
balancers.set(strategy, createCapabilityRouter(agents, balancerOptions));
|
|
893
|
+
break;
|
|
894
|
+
case 'custom':
|
|
895
|
+
// Custom strategies are handled separately
|
|
896
|
+
break;
|
|
897
|
+
default:
|
|
898
|
+
balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const balancer = balancers.get(strategy);
|
|
902
|
+
if (!balancer) {
|
|
903
|
+
// Fallback to round-robin if strategy not found
|
|
904
|
+
const fallback = createRoundRobinBalancer(agents, { metricsCollector: collector });
|
|
905
|
+
balancers.set(strategy, fallback);
|
|
906
|
+
return fallback;
|
|
907
|
+
}
|
|
908
|
+
return balancer;
|
|
909
|
+
}
|
|
910
|
+
function route(task) {
|
|
911
|
+
const start = performance.now();
|
|
912
|
+
const strategies = [];
|
|
913
|
+
const strategyScores = {};
|
|
914
|
+
let usedFallback = false;
|
|
915
|
+
// Handle weighted strategies
|
|
916
|
+
const weightedStrategies = config.strategies.map((s) => {
|
|
917
|
+
if (typeof s === 'string') {
|
|
918
|
+
return { strategy: s, weight: 1 };
|
|
919
|
+
}
|
|
920
|
+
return s;
|
|
921
|
+
});
|
|
922
|
+
// Try each strategy in order
|
|
923
|
+
for (const { strategy, weight } of weightedStrategies) {
|
|
924
|
+
strategies.push(strategy);
|
|
925
|
+
// Handle custom strategies
|
|
926
|
+
if (strategy === 'custom' && config.customStrategies) {
|
|
927
|
+
for (const [name, fn] of Object.entries(config.customStrategies)) {
|
|
928
|
+
const agent = fn(task, agents);
|
|
929
|
+
if (agent) {
|
|
930
|
+
const result = {
|
|
931
|
+
agent,
|
|
932
|
+
task,
|
|
933
|
+
strategy: 'custom',
|
|
934
|
+
timestamp: new Date(),
|
|
935
|
+
strategies,
|
|
936
|
+
strategyScores,
|
|
937
|
+
};
|
|
938
|
+
collector.record(result, performance.now() - start, 'custom');
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const balancer = getOrCreateBalancer(strategy);
|
|
945
|
+
const result = balancer.route(task);
|
|
946
|
+
if (result.agent) {
|
|
947
|
+
// Calculate score for weighted strategies
|
|
948
|
+
strategyScores[strategy] = weight;
|
|
949
|
+
const finalResult = {
|
|
950
|
+
...result,
|
|
951
|
+
strategies,
|
|
952
|
+
strategyScores,
|
|
953
|
+
usedFallback,
|
|
954
|
+
};
|
|
955
|
+
collector.record(finalResult, performance.now() - start, strategy);
|
|
956
|
+
return finalResult;
|
|
957
|
+
}
|
|
958
|
+
// Handle fallback
|
|
959
|
+
if (config.fallbackBehavior === 'next-strategy') {
|
|
960
|
+
usedFallback = true;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// No strategy succeeded
|
|
965
|
+
const result = {
|
|
966
|
+
agent: null,
|
|
967
|
+
task,
|
|
968
|
+
strategy: 'custom',
|
|
969
|
+
timestamp: new Date(),
|
|
970
|
+
reason: 'no-strategy-succeeded',
|
|
971
|
+
strategies,
|
|
972
|
+
strategyScores,
|
|
973
|
+
usedFallback,
|
|
974
|
+
};
|
|
975
|
+
collector.record(result, performance.now() - start, 'custom');
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
function addAgent(agent) {
|
|
979
|
+
agents.push(agent);
|
|
980
|
+
balancers.forEach((b) => b.addAgent(agent));
|
|
981
|
+
}
|
|
982
|
+
function removeAgent(agentId) {
|
|
983
|
+
agents = agents.filter((a) => a.id !== agentId);
|
|
984
|
+
balancers.forEach((b) => b.removeAgent(agentId));
|
|
985
|
+
}
|
|
986
|
+
function getAgents() {
|
|
987
|
+
return [...agents];
|
|
988
|
+
}
|
|
989
|
+
return { route, addAgent, removeAgent, getAgents };
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=load-balancing.js.map
|