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,1467 @@
|
|
|
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
|
+
import type { WorkerStatus, Contacts } from './types.js'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type Definitions
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Balancer strategy identifier
|
|
19
|
+
*/
|
|
20
|
+
export type BalancerStrategy =
|
|
21
|
+
| 'round-robin'
|
|
22
|
+
| 'least-busy'
|
|
23
|
+
| 'capability'
|
|
24
|
+
| 'priority-queue'
|
|
25
|
+
| 'custom'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extended agent information for load balancing
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentInfo {
|
|
31
|
+
id: string
|
|
32
|
+
name: string
|
|
33
|
+
type: 'agent'
|
|
34
|
+
status: WorkerStatus
|
|
35
|
+
skills: string[]
|
|
36
|
+
currentLoad: number
|
|
37
|
+
maxLoad: number
|
|
38
|
+
contacts: Contacts
|
|
39
|
+
metadata: Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Task request for routing
|
|
44
|
+
*/
|
|
45
|
+
export interface TaskRequest {
|
|
46
|
+
id: string
|
|
47
|
+
name: string
|
|
48
|
+
requiredSkills: string[]
|
|
49
|
+
priority: number
|
|
50
|
+
metadata: Record<string, unknown>
|
|
51
|
+
enqueuedAt?: Date
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Result of a routing decision
|
|
56
|
+
*/
|
|
57
|
+
export interface RouteResult {
|
|
58
|
+
agent: AgentInfo | null
|
|
59
|
+
task: TaskRequest
|
|
60
|
+
strategy: BalancerStrategy
|
|
61
|
+
timestamp: Date
|
|
62
|
+
reason?: string
|
|
63
|
+
matchScore?: number
|
|
64
|
+
matchedRule?: string | null
|
|
65
|
+
usedDefault?: boolean
|
|
66
|
+
usedFallback?: boolean
|
|
67
|
+
strategies?: BalancerStrategy[]
|
|
68
|
+
strategyScores?: Record<string, number>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Agent availability information
|
|
73
|
+
*/
|
|
74
|
+
export interface AgentAvailability {
|
|
75
|
+
status: WorkerStatus
|
|
76
|
+
lastSeen: Date
|
|
77
|
+
currentLoad?: number
|
|
78
|
+
maxLoad?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Routing rule definition
|
|
83
|
+
*/
|
|
84
|
+
export interface RoutingRule {
|
|
85
|
+
name: string
|
|
86
|
+
priority: number
|
|
87
|
+
condition: RoutingRuleCondition
|
|
88
|
+
action: (task: TaskRequest, agents: AgentInfo[]) => AgentInfo | null
|
|
89
|
+
enabled?: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Routing rule condition - function or declarative
|
|
94
|
+
*/
|
|
95
|
+
export type RoutingRuleCondition =
|
|
96
|
+
| ((task: TaskRequest) => boolean)
|
|
97
|
+
| {
|
|
98
|
+
requiredSkills?: { contains: string }
|
|
99
|
+
priority?: { gte?: number; lte?: number }
|
|
100
|
+
metadata?: Record<string, unknown>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Routing metrics
|
|
105
|
+
*/
|
|
106
|
+
export interface RoutingMetrics {
|
|
107
|
+
totalRouted: number
|
|
108
|
+
failedRoutes: number
|
|
109
|
+
averageLatencyMs: number
|
|
110
|
+
perAgent: Record<string, { routedCount: number; lastRouted?: Date }>
|
|
111
|
+
strategyUsage: Record<string, number>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Composite balancer configuration
|
|
116
|
+
*/
|
|
117
|
+
export interface CompositeBalancerConfig {
|
|
118
|
+
strategies: Array<BalancerStrategy | { strategy: BalancerStrategy; weight: number }>
|
|
119
|
+
fallbackBehavior?: 'next-strategy' | 'none'
|
|
120
|
+
customStrategies?: Record<string, (task: TaskRequest, agents: AgentInfo[]) => AgentInfo | null>
|
|
121
|
+
/**
|
|
122
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
123
|
+
* If not provided, uses the default global collector.
|
|
124
|
+
*/
|
|
125
|
+
metricsCollector?: MetricsCollector
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load balancer interface
|
|
130
|
+
*/
|
|
131
|
+
export interface LoadBalancer {
|
|
132
|
+
route(task: TaskRequest): RouteResult
|
|
133
|
+
addAgent(agent: AgentInfo): void
|
|
134
|
+
removeAgent(agentId: string): void
|
|
135
|
+
getAgents(): AgentInfo[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Metrics collector interface for thread-safe metrics collection.
|
|
140
|
+
*
|
|
141
|
+
* Each MetricsCollector instance maintains its own isolated state,
|
|
142
|
+
* allowing multiple balancers to either share a collector for
|
|
143
|
+
* aggregated metrics or use separate collectors for isolated tracking.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* Thread-safety is achieved through instance isolation. Each collector
|
|
147
|
+
* maintains its own metrics state, eliminating race conditions between
|
|
148
|
+
* different balancer instances. For shared metrics across multiple
|
|
149
|
+
* balancers, pass the same collector instance to each balancer.
|
|
150
|
+
*/
|
|
151
|
+
export interface MetricsCollector {
|
|
152
|
+
/**
|
|
153
|
+
* Record a routing event.
|
|
154
|
+
* @internal This method is called by balancers during routing.
|
|
155
|
+
*/
|
|
156
|
+
record(result: RouteResult, latencyMs: number, strategy: BalancerStrategy): void
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Collect current routing metrics.
|
|
160
|
+
* @returns A copy of the current metrics state
|
|
161
|
+
*/
|
|
162
|
+
collect(): RoutingMetrics
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reset all metrics to initial state.
|
|
166
|
+
*/
|
|
167
|
+
reset(): void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Safe Array Access Utilities
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Safely get the first element of an array.
|
|
176
|
+
*
|
|
177
|
+
* Provides type-safe access to array elements without non-null assertions.
|
|
178
|
+
*
|
|
179
|
+
* @param arr - The array to access
|
|
180
|
+
* @returns The first element or undefined if array is empty
|
|
181
|
+
*/
|
|
182
|
+
function safeFirst<T>(arr: T[]): T | undefined {
|
|
183
|
+
return arr.length > 0 ? arr[0] : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Safely get an element at a specific index.
|
|
188
|
+
*
|
|
189
|
+
* Provides type-safe access with bounds checking.
|
|
190
|
+
*
|
|
191
|
+
* @param arr - The array to access
|
|
192
|
+
* @param index - The index to access
|
|
193
|
+
* @returns The element at the index or undefined if out of bounds
|
|
194
|
+
*/
|
|
195
|
+
function safeAt<T>(arr: T[], index: number): T | undefined {
|
|
196
|
+
if (arr.length === 0 || index < 0 || index >= arr.length) {
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
return arr[index]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// MetricsCollector Implementation
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a new MetricsCollector instance with isolated state.
|
|
208
|
+
*
|
|
209
|
+
* This factory function creates a thread-safe metrics collector that
|
|
210
|
+
* encapsulates all metrics state within the returned instance. Multiple
|
|
211
|
+
* collectors can be used independently without interference.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* // Create isolated collectors for different environments
|
|
216
|
+
* const prodCollector = createMetricsCollector()
|
|
217
|
+
* const testCollector = createMetricsCollector()
|
|
218
|
+
*
|
|
219
|
+
* // Balancers with separate metrics
|
|
220
|
+
* const prodBalancer = createRoundRobinBalancer(agents, { metricsCollector: prodCollector })
|
|
221
|
+
* const testBalancer = createRoundRobinBalancer(agents, { metricsCollector: testCollector })
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* // Shared collector for aggregated metrics
|
|
227
|
+
* const sharedCollector = createMetricsCollector()
|
|
228
|
+
* const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
229
|
+
* const balancer2 = createLeastBusyBalancer(agents, { metricsCollector: sharedCollector })
|
|
230
|
+
*
|
|
231
|
+
* // Get combined metrics
|
|
232
|
+
* const metrics = sharedCollector.collect()
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* @returns A new MetricsCollector instance
|
|
236
|
+
*/
|
|
237
|
+
export function createMetricsCollector(): MetricsCollector {
|
|
238
|
+
// Instance-local state - each collector has its own isolated metrics
|
|
239
|
+
let metricsState: RoutingMetrics = {
|
|
240
|
+
totalRouted: 0,
|
|
241
|
+
failedRoutes: 0,
|
|
242
|
+
averageLatencyMs: 0,
|
|
243
|
+
perAgent: {},
|
|
244
|
+
strategyUsage: {},
|
|
245
|
+
}
|
|
246
|
+
let totalLatency = 0
|
|
247
|
+
|
|
248
|
+
function record(result: RouteResult, latencyMs: number, strategy: BalancerStrategy): void {
|
|
249
|
+
metricsState.totalRouted++
|
|
250
|
+
totalLatency += latencyMs
|
|
251
|
+
metricsState.averageLatencyMs = totalLatency / metricsState.totalRouted
|
|
252
|
+
|
|
253
|
+
if (!result.agent) {
|
|
254
|
+
metricsState.failedRoutes++
|
|
255
|
+
} else {
|
|
256
|
+
const agentId = result.agent.id
|
|
257
|
+
if (!metricsState.perAgent[agentId]) {
|
|
258
|
+
metricsState.perAgent[agentId] = { routedCount: 0 }
|
|
259
|
+
}
|
|
260
|
+
metricsState.perAgent[agentId].routedCount++
|
|
261
|
+
metricsState.perAgent[agentId].lastRouted = new Date()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
metricsState.strategyUsage[strategy] = (metricsState.strategyUsage[strategy] || 0) + 1
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collect(): RoutingMetrics {
|
|
268
|
+
// Return a deep copy to prevent external mutation
|
|
269
|
+
return {
|
|
270
|
+
...metricsState,
|
|
271
|
+
perAgent: { ...metricsState.perAgent },
|
|
272
|
+
strategyUsage: { ...metricsState.strategyUsage },
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function reset(): void {
|
|
277
|
+
metricsState = {
|
|
278
|
+
totalRouted: 0,
|
|
279
|
+
failedRoutes: 0,
|
|
280
|
+
averageLatencyMs: 0,
|
|
281
|
+
perAgent: {},
|
|
282
|
+
strategyUsage: {},
|
|
283
|
+
}
|
|
284
|
+
totalLatency = 0
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { record, collect, reset }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Default Global Metrics Collector (Backward Compatibility)
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Default metrics collector singleton for backward compatibility.
|
|
296
|
+
* Used when no explicit collector is provided to balancer factories.
|
|
297
|
+
*/
|
|
298
|
+
const defaultMetricsCollector = createMetricsCollector()
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Collect current routing metrics from the default global collector.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* This function is provided for backward compatibility. For new code,
|
|
305
|
+
* consider using explicit MetricsCollector instances for better isolation.
|
|
306
|
+
*
|
|
307
|
+
* @returns Current routing metrics
|
|
308
|
+
*/
|
|
309
|
+
export function collectRoutingMetrics(): RoutingMetrics {
|
|
310
|
+
return defaultMetricsCollector.collect()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Reset all routing metrics in the default global collector.
|
|
315
|
+
*
|
|
316
|
+
* @remarks
|
|
317
|
+
* This function is provided for backward compatibility. For new code,
|
|
318
|
+
* consider using explicit MetricsCollector instances for better isolation.
|
|
319
|
+
*/
|
|
320
|
+
export function resetRoutingMetrics(): void {
|
|
321
|
+
defaultMetricsCollector.reset()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Round-Robin Balancer
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Options for round-robin load balancer
|
|
330
|
+
*/
|
|
331
|
+
interface RoundRobinBalancerOptions {
|
|
332
|
+
/**
|
|
333
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
334
|
+
* If not provided, uses the default global collector.
|
|
335
|
+
*/
|
|
336
|
+
metricsCollector?: MetricsCollector
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create a round-robin load balancer
|
|
341
|
+
*
|
|
342
|
+
* Distributes tasks evenly across all available agents in order.
|
|
343
|
+
*
|
|
344
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
345
|
+
* @param options - Optional configuration including metricsCollector
|
|
346
|
+
* @returns A LoadBalancer instance
|
|
347
|
+
*/
|
|
348
|
+
export function createRoundRobinBalancer(
|
|
349
|
+
initialAgents: AgentInfo[],
|
|
350
|
+
options: RoundRobinBalancerOptions = {}
|
|
351
|
+
): LoadBalancer {
|
|
352
|
+
let agents = [...initialAgents]
|
|
353
|
+
let currentIndex = 0
|
|
354
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector
|
|
355
|
+
|
|
356
|
+
function getAvailableAgents(): AgentInfo[] {
|
|
357
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function route(task: TaskRequest): RouteResult {
|
|
361
|
+
const start = performance.now()
|
|
362
|
+
const available = getAvailableAgents()
|
|
363
|
+
|
|
364
|
+
if (available.length === 0) {
|
|
365
|
+
const result: RouteResult = {
|
|
366
|
+
agent: null,
|
|
367
|
+
task,
|
|
368
|
+
strategy: 'round-robin',
|
|
369
|
+
timestamp: new Date(),
|
|
370
|
+
reason: 'no-available-agents',
|
|
371
|
+
}
|
|
372
|
+
collector.record(result, performance.now() - start, 'round-robin')
|
|
373
|
+
return result
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Find the next available agent starting from current index
|
|
377
|
+
let attempts = 0
|
|
378
|
+
while (attempts < agents.length) {
|
|
379
|
+
const agent = safeAt(agents, currentIndex % agents.length)
|
|
380
|
+
currentIndex++
|
|
381
|
+
if (!agent) {
|
|
382
|
+
attempts++
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (agent.status === 'available' || agent.status === 'busy') {
|
|
387
|
+
const result: RouteResult = {
|
|
388
|
+
agent,
|
|
389
|
+
task,
|
|
390
|
+
strategy: 'round-robin',
|
|
391
|
+
timestamp: new Date(),
|
|
392
|
+
}
|
|
393
|
+
collector.record(result, performance.now() - start, 'round-robin')
|
|
394
|
+
return result
|
|
395
|
+
}
|
|
396
|
+
attempts++
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result: RouteResult = {
|
|
400
|
+
agent: null,
|
|
401
|
+
task,
|
|
402
|
+
strategy: 'round-robin',
|
|
403
|
+
timestamp: new Date(),
|
|
404
|
+
reason: 'no-available-agents',
|
|
405
|
+
}
|
|
406
|
+
collector.record(result, performance.now() - start, 'round-robin')
|
|
407
|
+
return result
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function addAgent(agent: AgentInfo): void {
|
|
411
|
+
agents.push(agent)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function removeAgent(agentId: string): void {
|
|
415
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getAgents(): AgentInfo[] {
|
|
419
|
+
return [...agents]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { route, addAgent, removeAgent, getAgents }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// Least-Busy Balancer
|
|
427
|
+
// ============================================================================
|
|
428
|
+
|
|
429
|
+
interface LeastBusyBalancer extends LoadBalancer {
|
|
430
|
+
getLoadMetrics(): Record<string, number>
|
|
431
|
+
releaseLoad(agentId: string): void
|
|
432
|
+
setLoad(agentId: string, load: number): void
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Options for least-busy load balancer
|
|
437
|
+
*/
|
|
438
|
+
interface LeastBusyBalancerOptions {
|
|
439
|
+
/**
|
|
440
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
441
|
+
* If not provided, uses the default global collector.
|
|
442
|
+
*/
|
|
443
|
+
metricsCollector?: MetricsCollector
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create a least-busy load balancer
|
|
448
|
+
*
|
|
449
|
+
* Routes tasks to agents with the lowest current load.
|
|
450
|
+
*
|
|
451
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
452
|
+
* @param options - Optional configuration including metricsCollector
|
|
453
|
+
* @returns A LeastBusyBalancer instance
|
|
454
|
+
*/
|
|
455
|
+
export function createLeastBusyBalancer(
|
|
456
|
+
initialAgents: AgentInfo[],
|
|
457
|
+
options: LeastBusyBalancerOptions = {}
|
|
458
|
+
): LeastBusyBalancer {
|
|
459
|
+
let agents = [...initialAgents]
|
|
460
|
+
const loadTracking = new Map<string, number>()
|
|
461
|
+
let lastRoutedIndex = -1
|
|
462
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector
|
|
463
|
+
|
|
464
|
+
// Initialize load tracking
|
|
465
|
+
agents.forEach((a) => loadTracking.set(a.id, a.currentLoad))
|
|
466
|
+
|
|
467
|
+
function getAvailableAgents(): AgentInfo[] {
|
|
468
|
+
return agents.filter((a) => {
|
|
469
|
+
if (a.status !== 'available' && a.status !== 'busy') return false
|
|
470
|
+
const load = loadTracking.get(a.id) ?? a.currentLoad
|
|
471
|
+
return load < a.maxLoad
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function route(task: TaskRequest): RouteResult {
|
|
476
|
+
const start = performance.now()
|
|
477
|
+
const available = getAvailableAgents()
|
|
478
|
+
|
|
479
|
+
if (available.length === 0) {
|
|
480
|
+
const result: RouteResult = {
|
|
481
|
+
agent: null,
|
|
482
|
+
task,
|
|
483
|
+
strategy: 'least-busy',
|
|
484
|
+
timestamp: new Date(),
|
|
485
|
+
reason: 'no-available-agents',
|
|
486
|
+
}
|
|
487
|
+
collector.record(result, performance.now() - start, 'least-busy')
|
|
488
|
+
return result
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Sort by load percentage
|
|
492
|
+
const sorted = [...available].sort((a, b) => {
|
|
493
|
+
const loadA = (loadTracking.get(a.id) ?? a.currentLoad) / a.maxLoad
|
|
494
|
+
const loadB = (loadTracking.get(b.id) ?? b.currentLoad) / b.maxLoad
|
|
495
|
+
if (loadA === loadB) {
|
|
496
|
+
// Tie-breaking with round-robin behavior
|
|
497
|
+
const indexA = agents.indexOf(a)
|
|
498
|
+
const indexB = agents.indexOf(b)
|
|
499
|
+
const distA = (indexA - lastRoutedIndex + agents.length) % agents.length
|
|
500
|
+
const distB = (indexB - lastRoutedIndex + agents.length) % agents.length
|
|
501
|
+
return distA - distB
|
|
502
|
+
}
|
|
503
|
+
return loadA - loadB
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
const selected = safeFirst(sorted)
|
|
507
|
+
if (!selected) {
|
|
508
|
+
const result: RouteResult = {
|
|
509
|
+
agent: null,
|
|
510
|
+
task,
|
|
511
|
+
strategy: 'least-busy',
|
|
512
|
+
timestamp: new Date(),
|
|
513
|
+
reason: 'no-available-agents',
|
|
514
|
+
}
|
|
515
|
+
collector.record(result, performance.now() - start, 'least-busy')
|
|
516
|
+
return result
|
|
517
|
+
}
|
|
518
|
+
lastRoutedIndex = agents.indexOf(selected)
|
|
519
|
+
|
|
520
|
+
// Increment load
|
|
521
|
+
const currentLoad = loadTracking.get(selected.id) ?? selected.currentLoad
|
|
522
|
+
loadTracking.set(selected.id, currentLoad + 1)
|
|
523
|
+
|
|
524
|
+
const result: RouteResult = {
|
|
525
|
+
agent: selected,
|
|
526
|
+
task,
|
|
527
|
+
strategy: 'least-busy',
|
|
528
|
+
timestamp: new Date(),
|
|
529
|
+
}
|
|
530
|
+
collector.record(result, performance.now() - start, 'least-busy')
|
|
531
|
+
return result
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function addAgent(agent: AgentInfo): void {
|
|
535
|
+
agents.push(agent)
|
|
536
|
+
loadTracking.set(agent.id, agent.currentLoad)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function removeAgent(agentId: string): void {
|
|
540
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
541
|
+
loadTracking.delete(agentId)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function getAgents(): AgentInfo[] {
|
|
545
|
+
return [...agents]
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function getLoadMetrics(): Record<string, number> {
|
|
549
|
+
const metrics: Record<string, number> = {}
|
|
550
|
+
agents.forEach((a) => {
|
|
551
|
+
const load = loadTracking.get(a.id) ?? a.currentLoad
|
|
552
|
+
metrics[a.id] = load / a.maxLoad
|
|
553
|
+
})
|
|
554
|
+
return metrics
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function releaseLoad(agentId: string): void {
|
|
558
|
+
const current = loadTracking.get(agentId)
|
|
559
|
+
if (current !== undefined && current > 0) {
|
|
560
|
+
loadTracking.set(agentId, current - 1)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setLoad(agentId: string, load: number): void {
|
|
565
|
+
loadTracking.set(agentId, load)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return { route, addAgent, removeAgent, getAgents, getLoadMetrics, releaseLoad, setLoad }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ============================================================================
|
|
572
|
+
// Capability-Based Router
|
|
573
|
+
// ============================================================================
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Options for capability-based router
|
|
577
|
+
*/
|
|
578
|
+
interface CapabilityRouterOptions {
|
|
579
|
+
/**
|
|
580
|
+
* Prefer agents with exact skill match over agents with additional skills
|
|
581
|
+
*/
|
|
582
|
+
preferExactMatch?: boolean
|
|
583
|
+
/**
|
|
584
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
585
|
+
* If not provided, uses the default global collector.
|
|
586
|
+
*/
|
|
587
|
+
metricsCollector?: MetricsCollector
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
interface CapabilityRouter extends LoadBalancer {
|
|
591
|
+
findAgentsWithSkills(skills: string[]): AgentInfo[]
|
|
592
|
+
getSkillCoverage(): Record<string, number>
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Create a capability-based router
|
|
597
|
+
*
|
|
598
|
+
* Routes tasks to agents that have the required skills.
|
|
599
|
+
*
|
|
600
|
+
* @param initialAgents - Initial set of agents to route to
|
|
601
|
+
* @param options - Optional configuration including metricsCollector
|
|
602
|
+
* @returns A CapabilityRouter instance
|
|
603
|
+
*/
|
|
604
|
+
export function createCapabilityRouter(
|
|
605
|
+
initialAgents: AgentInfo[],
|
|
606
|
+
options: CapabilityRouterOptions = {}
|
|
607
|
+
): CapabilityRouter {
|
|
608
|
+
let agents = [...initialAgents]
|
|
609
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector
|
|
610
|
+
|
|
611
|
+
function getAvailableAgents(): AgentInfo[] {
|
|
612
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy')
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function hasAllSkills(agent: AgentInfo, requiredSkills: string[]): boolean {
|
|
616
|
+
return requiredSkills.every((skill) => agent.skills.includes(skill))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function calculateMatchScore(agent: AgentInfo, requiredSkills: string[]): number {
|
|
620
|
+
if (requiredSkills.length === 0) return 1
|
|
621
|
+
const matchingSkills = requiredSkills.filter((s) => agent.skills.includes(s))
|
|
622
|
+
return matchingSkills.length / requiredSkills.length
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function route(task: TaskRequest): RouteResult {
|
|
626
|
+
const start = performance.now()
|
|
627
|
+
const available = getAvailableAgents()
|
|
628
|
+
|
|
629
|
+
// Find agents with all required skills
|
|
630
|
+
let candidates = available.filter((a) => hasAllSkills(a, task.requiredSkills))
|
|
631
|
+
|
|
632
|
+
if (candidates.length === 0) {
|
|
633
|
+
const result: RouteResult = {
|
|
634
|
+
agent: null,
|
|
635
|
+
task,
|
|
636
|
+
strategy: 'capability',
|
|
637
|
+
timestamp: new Date(),
|
|
638
|
+
reason: 'no-matching-capability',
|
|
639
|
+
}
|
|
640
|
+
collector.record(result, performance.now() - start, 'capability')
|
|
641
|
+
return result
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Sort by match quality
|
|
645
|
+
if (options.preferExactMatch) {
|
|
646
|
+
// Prefer agents with closest skill count to requirements
|
|
647
|
+
candidates.sort((a, b) => {
|
|
648
|
+
const diffA = Math.abs(a.skills.length - task.requiredSkills.length)
|
|
649
|
+
const diffB = Math.abs(b.skills.length - task.requiredSkills.length)
|
|
650
|
+
return diffA - diffB
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const selected = safeFirst(candidates)
|
|
655
|
+
if (!selected) {
|
|
656
|
+
const result: RouteResult = {
|
|
657
|
+
agent: null,
|
|
658
|
+
task,
|
|
659
|
+
strategy: 'capability',
|
|
660
|
+
timestamp: new Date(),
|
|
661
|
+
reason: 'no-matching-capability',
|
|
662
|
+
}
|
|
663
|
+
collector.record(result, performance.now() - start, 'capability')
|
|
664
|
+
return result
|
|
665
|
+
}
|
|
666
|
+
const matchScore = calculateMatchScore(selected, task.requiredSkills)
|
|
667
|
+
|
|
668
|
+
const result: RouteResult = {
|
|
669
|
+
agent: selected,
|
|
670
|
+
task,
|
|
671
|
+
strategy: 'capability',
|
|
672
|
+
timestamp: new Date(),
|
|
673
|
+
matchScore,
|
|
674
|
+
}
|
|
675
|
+
collector.record(result, performance.now() - start, 'capability')
|
|
676
|
+
return result
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function addAgent(agent: AgentInfo): void {
|
|
680
|
+
agents.push(agent)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function removeAgent(agentId: string): void {
|
|
684
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function getAgents(): AgentInfo[] {
|
|
688
|
+
return [...agents]
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function findAgentsWithSkills(skills: string[]): AgentInfo[] {
|
|
692
|
+
return agents.filter((a) => hasAllSkills(a, skills))
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getSkillCoverage(): Record<string, number> {
|
|
696
|
+
const coverage: Record<string, number> = {}
|
|
697
|
+
agents.forEach((a) => {
|
|
698
|
+
a.skills.forEach((skill) => {
|
|
699
|
+
coverage[skill] = (coverage[skill] || 0) + 1
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
return coverage
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return { route, addAgent, removeAgent, getAgents, findAgentsWithSkills, getSkillCoverage }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ============================================================================
|
|
709
|
+
// Priority Queue Balancer
|
|
710
|
+
// ============================================================================
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Options for priority queue balancer
|
|
714
|
+
*/
|
|
715
|
+
interface PriorityQueueOptions {
|
|
716
|
+
/**
|
|
717
|
+
* Enable priority aging to prevent task starvation
|
|
718
|
+
*/
|
|
719
|
+
enableAging?: boolean
|
|
720
|
+
/**
|
|
721
|
+
* Priority boost per second when aging is enabled
|
|
722
|
+
*/
|
|
723
|
+
agingBoostPerSecond?: number
|
|
724
|
+
/**
|
|
725
|
+
* Maximum wait time before task is promoted to highest priority
|
|
726
|
+
*/
|
|
727
|
+
maxWaitTime?: number
|
|
728
|
+
/**
|
|
729
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
730
|
+
* If not provided, uses the default global collector.
|
|
731
|
+
*/
|
|
732
|
+
metricsCollector?: MetricsCollector
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
interface PriorityQueueBalancer extends LoadBalancer {
|
|
736
|
+
enqueue(task: TaskRequest): void
|
|
737
|
+
routeNext(): Promise<RouteResult | null>
|
|
738
|
+
queueSize(): number
|
|
739
|
+
clear(): void
|
|
740
|
+
peek(): TaskRequest | null
|
|
741
|
+
getEffectivePriority(taskId: string): number
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Create a priority queue balancer
|
|
746
|
+
*
|
|
747
|
+
* Processes tasks in priority order with optional aging to prevent starvation.
|
|
748
|
+
*
|
|
749
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
750
|
+
* @param options - Optional configuration including metricsCollector
|
|
751
|
+
* @returns A PriorityQueueBalancer instance
|
|
752
|
+
*/
|
|
753
|
+
export function createPriorityQueueBalancer(
|
|
754
|
+
initialAgents: AgentInfo[],
|
|
755
|
+
options: PriorityQueueOptions = {}
|
|
756
|
+
): PriorityQueueBalancer {
|
|
757
|
+
let agents = [...initialAgents]
|
|
758
|
+
const queue: TaskRequest[] = []
|
|
759
|
+
const { enableAging = false, agingBoostPerSecond = 1, maxWaitTime } = options
|
|
760
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector
|
|
761
|
+
|
|
762
|
+
function getAvailableAgents(): AgentInfo[] {
|
|
763
|
+
return agents.filter((a) => a.status === 'available' || a.status === 'busy')
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function getEffectivePriority(taskId: string): number {
|
|
767
|
+
const task = queue.find((t) => t.id === taskId)
|
|
768
|
+
if (!task) return 0
|
|
769
|
+
|
|
770
|
+
let priority = task.priority
|
|
771
|
+
|
|
772
|
+
if (enableAging && task.enqueuedAt) {
|
|
773
|
+
const waitTimeMs = Date.now() - task.enqueuedAt.getTime()
|
|
774
|
+
const waitTimeSeconds = waitTimeMs / 1000
|
|
775
|
+
priority += waitTimeSeconds * agingBoostPerSecond
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (maxWaitTime && task.enqueuedAt) {
|
|
779
|
+
const waitTimeMs = Date.now() - task.enqueuedAt.getTime()
|
|
780
|
+
if (waitTimeMs >= maxWaitTime) {
|
|
781
|
+
priority = Infinity // Promote to highest priority
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return priority
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function sortQueue(): void {
|
|
789
|
+
queue.sort((a, b) => {
|
|
790
|
+
const priorityA = getEffectivePriority(a.id)
|
|
791
|
+
const priorityB = getEffectivePriority(b.id)
|
|
792
|
+
if (priorityB !== priorityA) {
|
|
793
|
+
return priorityB - priorityA // Higher priority first
|
|
794
|
+
}
|
|
795
|
+
// FIFO for equal priority
|
|
796
|
+
const timeA = a.enqueuedAt?.getTime() ?? 0
|
|
797
|
+
const timeB = b.enqueuedAt?.getTime() ?? 0
|
|
798
|
+
return timeA - timeB
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function enqueue(task: TaskRequest): void {
|
|
803
|
+
if (task.priority < 1 || task.priority > 10) {
|
|
804
|
+
throw new Error('Priority must be between 1 and 10')
|
|
805
|
+
}
|
|
806
|
+
const taskWithTime = { ...task, enqueuedAt: new Date() }
|
|
807
|
+
queue.push(taskWithTime)
|
|
808
|
+
sortQueue()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function routeNext(): Promise<RouteResult | null> {
|
|
812
|
+
if (queue.length === 0) return null
|
|
813
|
+
|
|
814
|
+
sortQueue()
|
|
815
|
+
const task = queue.shift()
|
|
816
|
+
if (!task) return null
|
|
817
|
+
const start = performance.now()
|
|
818
|
+
const available = getAvailableAgents()
|
|
819
|
+
|
|
820
|
+
if (available.length === 0) {
|
|
821
|
+
const result: RouteResult = {
|
|
822
|
+
agent: null,
|
|
823
|
+
task,
|
|
824
|
+
strategy: 'priority-queue',
|
|
825
|
+
timestamp: new Date(),
|
|
826
|
+
reason: 'no-available-agents',
|
|
827
|
+
}
|
|
828
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
829
|
+
return result
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Simple round-robin among available for now
|
|
833
|
+
const agent = safeFirst(available)
|
|
834
|
+
if (!agent) {
|
|
835
|
+
const result: RouteResult = {
|
|
836
|
+
agent: null,
|
|
837
|
+
task,
|
|
838
|
+
strategy: 'priority-queue',
|
|
839
|
+
timestamp: new Date(),
|
|
840
|
+
reason: 'no-available-agents',
|
|
841
|
+
}
|
|
842
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
843
|
+
return result
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const result: RouteResult = {
|
|
847
|
+
agent,
|
|
848
|
+
task,
|
|
849
|
+
strategy: 'priority-queue',
|
|
850
|
+
timestamp: new Date(),
|
|
851
|
+
}
|
|
852
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
853
|
+
return result
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function route(task: TaskRequest): RouteResult {
|
|
857
|
+
const start = performance.now()
|
|
858
|
+
const available = getAvailableAgents()
|
|
859
|
+
|
|
860
|
+
if (available.length === 0) {
|
|
861
|
+
const result: RouteResult = {
|
|
862
|
+
agent: null,
|
|
863
|
+
task,
|
|
864
|
+
strategy: 'priority-queue',
|
|
865
|
+
timestamp: new Date(),
|
|
866
|
+
reason: 'no-available-agents',
|
|
867
|
+
}
|
|
868
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
869
|
+
return result
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const agent = safeFirst(available)
|
|
873
|
+
if (!agent) {
|
|
874
|
+
const result: RouteResult = {
|
|
875
|
+
agent: null,
|
|
876
|
+
task,
|
|
877
|
+
strategy: 'priority-queue',
|
|
878
|
+
timestamp: new Date(),
|
|
879
|
+
reason: 'no-available-agents',
|
|
880
|
+
}
|
|
881
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
882
|
+
return result
|
|
883
|
+
}
|
|
884
|
+
const result: RouteResult = {
|
|
885
|
+
agent,
|
|
886
|
+
task,
|
|
887
|
+
strategy: 'priority-queue',
|
|
888
|
+
timestamp: new Date(),
|
|
889
|
+
}
|
|
890
|
+
collector.record(result, performance.now() - start, 'priority-queue')
|
|
891
|
+
return result
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function addAgent(agent: AgentInfo): void {
|
|
895
|
+
agents.push(agent)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function removeAgent(agentId: string): void {
|
|
899
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function getAgents(): AgentInfo[] {
|
|
903
|
+
return [...agents]
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function queueSize(): number {
|
|
907
|
+
return queue.length
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function clear(): void {
|
|
911
|
+
queue.length = 0
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function peek(): TaskRequest | null {
|
|
915
|
+
if (queue.length === 0) return null
|
|
916
|
+
sortQueue()
|
|
917
|
+
return queue[0] ?? null
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
route,
|
|
922
|
+
addAgent,
|
|
923
|
+
removeAgent,
|
|
924
|
+
getAgents,
|
|
925
|
+
enqueue,
|
|
926
|
+
routeNext,
|
|
927
|
+
queueSize,
|
|
928
|
+
clear,
|
|
929
|
+
peek,
|
|
930
|
+
getEffectivePriority,
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// Agent Availability Tracker
|
|
936
|
+
// ============================================================================
|
|
937
|
+
|
|
938
|
+
interface AvailabilityTrackerOptions {
|
|
939
|
+
heartbeatTimeout?: number
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
interface StatusChangeEvent {
|
|
943
|
+
agentId: string
|
|
944
|
+
previousStatus: WorkerStatus
|
|
945
|
+
currentStatus: WorkerStatus
|
|
946
|
+
timestamp: Date
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
interface CapacityInfo {
|
|
950
|
+
total: number
|
|
951
|
+
used: number
|
|
952
|
+
available: number
|
|
953
|
+
utilization: number
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
interface AgentAvailabilityTracker {
|
|
957
|
+
getAvailability(agentId: string): AgentAvailability
|
|
958
|
+
updateStatus(agentId: string, status: WorkerStatus): void
|
|
959
|
+
getAvailableAgents(): AgentInfo[]
|
|
960
|
+
heartbeat(agentId: string): void
|
|
961
|
+
checkTimeouts(): void
|
|
962
|
+
onStatusChange(handler: (event: StatusChangeEvent) => void): void
|
|
963
|
+
updateLoad(agentId: string, current: number, max: number): void
|
|
964
|
+
getCapacityUtilization(): Record<string, number>
|
|
965
|
+
getOverallCapacity(): CapacityInfo
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Create an agent availability tracker
|
|
970
|
+
*
|
|
971
|
+
* Tracks agent status, heartbeats, and capacity.
|
|
972
|
+
*/
|
|
973
|
+
export function createAgentAvailabilityTracker(
|
|
974
|
+
initialAgents: AgentInfo[],
|
|
975
|
+
options: AvailabilityTrackerOptions = {}
|
|
976
|
+
): AgentAvailabilityTracker {
|
|
977
|
+
const agents = new Map<string, AgentInfo>()
|
|
978
|
+
const availability = new Map<string, AgentAvailability>()
|
|
979
|
+
const handlers: Array<(event: StatusChangeEvent) => void> = []
|
|
980
|
+
const { heartbeatTimeout = 30000 } = options
|
|
981
|
+
|
|
982
|
+
// Initialize
|
|
983
|
+
initialAgents.forEach((a) => {
|
|
984
|
+
agents.set(a.id, a)
|
|
985
|
+
availability.set(a.id, {
|
|
986
|
+
status: a.status,
|
|
987
|
+
lastSeen: new Date(),
|
|
988
|
+
currentLoad: a.currentLoad,
|
|
989
|
+
maxLoad: a.maxLoad,
|
|
990
|
+
})
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
function getAvailability(agentId: string): AgentAvailability {
|
|
994
|
+
return (
|
|
995
|
+
availability.get(agentId) ?? {
|
|
996
|
+
status: 'offline',
|
|
997
|
+
lastSeen: new Date(0),
|
|
998
|
+
}
|
|
999
|
+
)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function updateStatus(agentId: string, status: WorkerStatus): void {
|
|
1003
|
+
const current = availability.get(agentId)
|
|
1004
|
+
const previousStatus = current?.status ?? 'offline'
|
|
1005
|
+
|
|
1006
|
+
availability.set(agentId, {
|
|
1007
|
+
...current,
|
|
1008
|
+
status,
|
|
1009
|
+
lastSeen: new Date(),
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
const agent = agents.get(agentId)
|
|
1013
|
+
if (agent) {
|
|
1014
|
+
agent.status = status
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Emit status change event
|
|
1018
|
+
if (previousStatus !== status) {
|
|
1019
|
+
const event: StatusChangeEvent = {
|
|
1020
|
+
agentId,
|
|
1021
|
+
previousStatus,
|
|
1022
|
+
currentStatus: status,
|
|
1023
|
+
timestamp: new Date(),
|
|
1024
|
+
}
|
|
1025
|
+
handlers.forEach((h) => h(event))
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function getAvailableAgents(): AgentInfo[] {
|
|
1030
|
+
return Array.from(agents.values()).filter((a) => {
|
|
1031
|
+
const avail = availability.get(a.id)
|
|
1032
|
+
return avail?.status === 'available' || avail?.status === 'busy'
|
|
1033
|
+
})
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function heartbeat(agentId: string): void {
|
|
1037
|
+
const current = availability.get(agentId)
|
|
1038
|
+
if (current) {
|
|
1039
|
+
availability.set(agentId, {
|
|
1040
|
+
...current,
|
|
1041
|
+
lastSeen: new Date(),
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function checkTimeouts(): void {
|
|
1047
|
+
const now = Date.now()
|
|
1048
|
+
availability.forEach((avail, agentId) => {
|
|
1049
|
+
if (avail.status !== 'offline') {
|
|
1050
|
+
const timeSinceLastSeen = now - avail.lastSeen.getTime()
|
|
1051
|
+
if (timeSinceLastSeen > heartbeatTimeout) {
|
|
1052
|
+
updateStatus(agentId, 'offline')
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function onStatusChange(handler: (event: StatusChangeEvent) => void): void {
|
|
1059
|
+
handlers.push(handler)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function updateLoad(agentId: string, current: number, max: number): void {
|
|
1063
|
+
const avail = availability.get(agentId)
|
|
1064
|
+
if (avail) {
|
|
1065
|
+
availability.set(agentId, {
|
|
1066
|
+
...avail,
|
|
1067
|
+
currentLoad: current,
|
|
1068
|
+
maxLoad: max,
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function getCapacityUtilization(): Record<string, number> {
|
|
1074
|
+
const result: Record<string, number> = {}
|
|
1075
|
+
availability.forEach((avail, agentId) => {
|
|
1076
|
+
if (avail.currentLoad !== undefined && avail.maxLoad !== undefined && avail.maxLoad > 0) {
|
|
1077
|
+
result[agentId] = avail.currentLoad / avail.maxLoad
|
|
1078
|
+
}
|
|
1079
|
+
})
|
|
1080
|
+
return result
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function getOverallCapacity(): CapacityInfo {
|
|
1084
|
+
let total = 0
|
|
1085
|
+
let used = 0
|
|
1086
|
+
|
|
1087
|
+
availability.forEach((avail, agentId) => {
|
|
1088
|
+
const agent = agents.get(agentId)
|
|
1089
|
+
if (agent && (avail.status === 'available' || avail.status === 'busy')) {
|
|
1090
|
+
total += avail.maxLoad ?? agent.maxLoad
|
|
1091
|
+
used += avail.currentLoad ?? agent.currentLoad
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
total,
|
|
1097
|
+
used,
|
|
1098
|
+
available: total - used,
|
|
1099
|
+
utilization: total > 0 ? used / total : 0,
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return {
|
|
1104
|
+
getAvailability,
|
|
1105
|
+
updateStatus,
|
|
1106
|
+
getAvailableAgents,
|
|
1107
|
+
heartbeat,
|
|
1108
|
+
checkTimeouts,
|
|
1109
|
+
onStatusChange,
|
|
1110
|
+
updateLoad,
|
|
1111
|
+
getCapacityUtilization,
|
|
1112
|
+
getOverallCapacity,
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ============================================================================
|
|
1117
|
+
// Routing Rule Engine
|
|
1118
|
+
// ============================================================================
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Options for routing rule engine
|
|
1122
|
+
*/
|
|
1123
|
+
interface RoutingRuleEngineOptions {
|
|
1124
|
+
/**
|
|
1125
|
+
* Default strategy to use when no rules match
|
|
1126
|
+
*/
|
|
1127
|
+
defaultStrategy?: 'round-robin' | 'least-busy' | 'capability'
|
|
1128
|
+
/**
|
|
1129
|
+
* Optional metrics collector for isolated metrics tracking.
|
|
1130
|
+
* If not provided, uses the default global collector.
|
|
1131
|
+
*/
|
|
1132
|
+
metricsCollector?: MetricsCollector
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
interface RoutingRuleEngine extends LoadBalancer {
|
|
1136
|
+
addRule(rule: RoutingRule): void
|
|
1137
|
+
removeRule(name: string): void
|
|
1138
|
+
updateRule(name: string, updates: Partial<RoutingRule>): void
|
|
1139
|
+
enableRule(name: string): void
|
|
1140
|
+
disableRule(name: string): void
|
|
1141
|
+
getRules(): RoutingRule[]
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Create a routing rule engine
|
|
1146
|
+
*
|
|
1147
|
+
* Evaluates routing rules in priority order to determine task routing.
|
|
1148
|
+
*
|
|
1149
|
+
* @param initialAgents - Initial set of agents to route to
|
|
1150
|
+
* @param options - Optional configuration including metricsCollector
|
|
1151
|
+
* @returns A RoutingRuleEngine instance
|
|
1152
|
+
*/
|
|
1153
|
+
export function createRoutingRuleEngine(
|
|
1154
|
+
initialAgents: AgentInfo[],
|
|
1155
|
+
options: RoutingRuleEngineOptions = {}
|
|
1156
|
+
): RoutingRuleEngine {
|
|
1157
|
+
let agents = [...initialAgents]
|
|
1158
|
+
const rules: RoutingRule[] = []
|
|
1159
|
+
const { defaultStrategy = 'round-robin' } = options
|
|
1160
|
+
const collector = options.metricsCollector ?? defaultMetricsCollector
|
|
1161
|
+
|
|
1162
|
+
// Create default balancer for fallback
|
|
1163
|
+
let defaultBalancer: LoadBalancer
|
|
1164
|
+
|
|
1165
|
+
function getDefaultBalancer(): LoadBalancer {
|
|
1166
|
+
if (!defaultBalancer) {
|
|
1167
|
+
const balancerOptions = { metricsCollector: collector }
|
|
1168
|
+
switch (defaultStrategy) {
|
|
1169
|
+
case 'least-busy':
|
|
1170
|
+
defaultBalancer = createLeastBusyBalancer(agents, balancerOptions)
|
|
1171
|
+
break
|
|
1172
|
+
case 'capability':
|
|
1173
|
+
defaultBalancer = createCapabilityRouter(agents, balancerOptions)
|
|
1174
|
+
break
|
|
1175
|
+
default:
|
|
1176
|
+
defaultBalancer = createRoundRobinBalancer(agents, balancerOptions)
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return defaultBalancer
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function evaluateCondition(condition: RoutingRuleCondition, task: TaskRequest): boolean {
|
|
1183
|
+
if (typeof condition === 'function') {
|
|
1184
|
+
return condition(task)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Declarative condition evaluation
|
|
1188
|
+
if (condition.requiredSkills?.contains) {
|
|
1189
|
+
if (!task.requiredSkills.includes(condition.requiredSkills.contains)) {
|
|
1190
|
+
return false
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (condition.priority) {
|
|
1195
|
+
if (condition.priority.gte !== undefined && task.priority < condition.priority.gte) {
|
|
1196
|
+
return false
|
|
1197
|
+
}
|
|
1198
|
+
if (condition.priority.lte !== undefined && task.priority > condition.priority.lte) {
|
|
1199
|
+
return false
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (condition.metadata) {
|
|
1204
|
+
for (const [key, value] of Object.entries(condition.metadata)) {
|
|
1205
|
+
if (task.metadata[key] !== value) {
|
|
1206
|
+
return false
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return true
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function route(task: TaskRequest): RouteResult {
|
|
1215
|
+
const start = performance.now()
|
|
1216
|
+
|
|
1217
|
+
// Sort rules by priority (descending)
|
|
1218
|
+
const sortedRules = [...rules]
|
|
1219
|
+
.filter((r) => r.enabled !== false)
|
|
1220
|
+
.sort((a, b) => b.priority - a.priority)
|
|
1221
|
+
|
|
1222
|
+
// Evaluate rules
|
|
1223
|
+
for (const rule of sortedRules) {
|
|
1224
|
+
if (evaluateCondition(rule.condition, task)) {
|
|
1225
|
+
const agent = rule.action(task, agents)
|
|
1226
|
+
if (agent) {
|
|
1227
|
+
const result: RouteResult = {
|
|
1228
|
+
agent,
|
|
1229
|
+
task,
|
|
1230
|
+
strategy: 'custom',
|
|
1231
|
+
timestamp: new Date(),
|
|
1232
|
+
matchedRule: rule.name,
|
|
1233
|
+
}
|
|
1234
|
+
collector.record(result, performance.now() - start, 'custom')
|
|
1235
|
+
return result
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Use default strategy as fallback
|
|
1241
|
+
const defaultResult = getDefaultBalancer().route(task)
|
|
1242
|
+
return {
|
|
1243
|
+
...defaultResult,
|
|
1244
|
+
matchedRule: null,
|
|
1245
|
+
usedDefault: true,
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function addRule(rule: RoutingRule): void {
|
|
1250
|
+
if (!rule.name || rule.name.trim() === '') {
|
|
1251
|
+
throw new Error('Rule name is required')
|
|
1252
|
+
}
|
|
1253
|
+
if (rule.priority < 0) {
|
|
1254
|
+
throw new Error('Rule priority must be non-negative')
|
|
1255
|
+
}
|
|
1256
|
+
rules.push({ ...rule, enabled: rule.enabled ?? true })
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function removeRule(name: string): void {
|
|
1260
|
+
const index = rules.findIndex((r) => r.name === name)
|
|
1261
|
+
if (index !== -1) {
|
|
1262
|
+
rules.splice(index, 1)
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function updateRule(name: string, updates: Partial<RoutingRule>): void {
|
|
1267
|
+
const rule = rules.find((r) => r.name === name)
|
|
1268
|
+
if (rule) {
|
|
1269
|
+
Object.assign(rule, updates)
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function enableRule(name: string): void {
|
|
1274
|
+
const rule = rules.find((r) => r.name === name)
|
|
1275
|
+
if (rule) {
|
|
1276
|
+
rule.enabled = true
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function disableRule(name: string): void {
|
|
1281
|
+
const rule = rules.find((r) => r.name === name)
|
|
1282
|
+
if (rule) {
|
|
1283
|
+
rule.enabled = false
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function getRules(): RoutingRule[] {
|
|
1288
|
+
return [...rules]
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function addAgent(agent: AgentInfo): void {
|
|
1292
|
+
agents.push(agent)
|
|
1293
|
+
defaultBalancer = undefined as any // Reset default balancer
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function removeAgent(agentId: string): void {
|
|
1297
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
1298
|
+
defaultBalancer = undefined as any // Reset default balancer
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function getAgents(): AgentInfo[] {
|
|
1302
|
+
return [...agents]
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
route,
|
|
1307
|
+
addAgent,
|
|
1308
|
+
removeAgent,
|
|
1309
|
+
getAgents,
|
|
1310
|
+
addRule,
|
|
1311
|
+
removeRule,
|
|
1312
|
+
updateRule,
|
|
1313
|
+
enableRule,
|
|
1314
|
+
disableRule,
|
|
1315
|
+
getRules,
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// ============================================================================
|
|
1320
|
+
// Composite Balancer
|
|
1321
|
+
// ============================================================================
|
|
1322
|
+
|
|
1323
|
+
interface CompositeBalancer extends LoadBalancer {
|
|
1324
|
+
// No additional methods for now
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Create a composite load balancer
|
|
1329
|
+
*
|
|
1330
|
+
* Combines multiple balancing strategies for sophisticated routing decisions.
|
|
1331
|
+
*
|
|
1332
|
+
* @param initialAgents - Initial set of agents to balance across
|
|
1333
|
+
* @param config - Configuration including strategies and optional metricsCollector
|
|
1334
|
+
* @returns A CompositeBalancer instance
|
|
1335
|
+
*/
|
|
1336
|
+
export function createCompositeBalancer(
|
|
1337
|
+
initialAgents: AgentInfo[],
|
|
1338
|
+
config: CompositeBalancerConfig
|
|
1339
|
+
): CompositeBalancer {
|
|
1340
|
+
let agents = [...initialAgents]
|
|
1341
|
+
const balancers = new Map<BalancerStrategy, LoadBalancer>()
|
|
1342
|
+
const collector = config.metricsCollector ?? defaultMetricsCollector
|
|
1343
|
+
|
|
1344
|
+
// Initialize balancers
|
|
1345
|
+
function getOrCreateBalancer(strategy: BalancerStrategy): LoadBalancer {
|
|
1346
|
+
if (!balancers.has(strategy)) {
|
|
1347
|
+
const balancerOptions = { metricsCollector: collector }
|
|
1348
|
+
switch (strategy) {
|
|
1349
|
+
case 'round-robin':
|
|
1350
|
+
balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions))
|
|
1351
|
+
break
|
|
1352
|
+
case 'least-busy':
|
|
1353
|
+
balancers.set(strategy, createLeastBusyBalancer(agents, balancerOptions))
|
|
1354
|
+
break
|
|
1355
|
+
case 'capability':
|
|
1356
|
+
balancers.set(strategy, createCapabilityRouter(agents, balancerOptions))
|
|
1357
|
+
break
|
|
1358
|
+
case 'custom':
|
|
1359
|
+
// Custom strategies are handled separately
|
|
1360
|
+
break
|
|
1361
|
+
default:
|
|
1362
|
+
balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions))
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const balancer = balancers.get(strategy)
|
|
1366
|
+
if (!balancer) {
|
|
1367
|
+
// Fallback to round-robin if strategy not found
|
|
1368
|
+
const fallback = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
1369
|
+
balancers.set(strategy, fallback)
|
|
1370
|
+
return fallback
|
|
1371
|
+
}
|
|
1372
|
+
return balancer
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function route(task: TaskRequest): RouteResult {
|
|
1376
|
+
const start = performance.now()
|
|
1377
|
+
const strategies: BalancerStrategy[] = []
|
|
1378
|
+
const strategyScores: Record<string, number> = {}
|
|
1379
|
+
let usedFallback = false
|
|
1380
|
+
|
|
1381
|
+
// Handle weighted strategies
|
|
1382
|
+
const weightedStrategies = config.strategies.map((s) => {
|
|
1383
|
+
if (typeof s === 'string') {
|
|
1384
|
+
return { strategy: s, weight: 1 }
|
|
1385
|
+
}
|
|
1386
|
+
return s
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
// Try each strategy in order
|
|
1390
|
+
for (const { strategy, weight } of weightedStrategies) {
|
|
1391
|
+
strategies.push(strategy)
|
|
1392
|
+
|
|
1393
|
+
// Handle custom strategies
|
|
1394
|
+
if (strategy === 'custom' && config.customStrategies) {
|
|
1395
|
+
for (const [name, fn] of Object.entries(config.customStrategies)) {
|
|
1396
|
+
const agent = fn(task, agents)
|
|
1397
|
+
if (agent) {
|
|
1398
|
+
const result: RouteResult = {
|
|
1399
|
+
agent,
|
|
1400
|
+
task,
|
|
1401
|
+
strategy: 'custom',
|
|
1402
|
+
timestamp: new Date(),
|
|
1403
|
+
strategies,
|
|
1404
|
+
strategyScores,
|
|
1405
|
+
}
|
|
1406
|
+
collector.record(result, performance.now() - start, 'custom')
|
|
1407
|
+
return result
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
continue
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const balancer = getOrCreateBalancer(strategy)
|
|
1414
|
+
const result = balancer.route(task)
|
|
1415
|
+
|
|
1416
|
+
if (result.agent) {
|
|
1417
|
+
// Calculate score for weighted strategies
|
|
1418
|
+
strategyScores[strategy] = weight
|
|
1419
|
+
|
|
1420
|
+
const finalResult: RouteResult = {
|
|
1421
|
+
...result,
|
|
1422
|
+
strategies,
|
|
1423
|
+
strategyScores,
|
|
1424
|
+
usedFallback,
|
|
1425
|
+
}
|
|
1426
|
+
collector.record(finalResult, performance.now() - start, strategy)
|
|
1427
|
+
return finalResult
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Handle fallback
|
|
1431
|
+
if (config.fallbackBehavior === 'next-strategy') {
|
|
1432
|
+
usedFallback = true
|
|
1433
|
+
continue
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// No strategy succeeded
|
|
1438
|
+
const result: RouteResult = {
|
|
1439
|
+
agent: null,
|
|
1440
|
+
task,
|
|
1441
|
+
strategy: 'custom',
|
|
1442
|
+
timestamp: new Date(),
|
|
1443
|
+
reason: 'no-strategy-succeeded',
|
|
1444
|
+
strategies,
|
|
1445
|
+
strategyScores,
|
|
1446
|
+
usedFallback,
|
|
1447
|
+
}
|
|
1448
|
+
collector.record(result, performance.now() - start, 'custom')
|
|
1449
|
+
return result
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function addAgent(agent: AgentInfo): void {
|
|
1453
|
+
agents.push(agent)
|
|
1454
|
+
balancers.forEach((b) => b.addAgent(agent))
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function removeAgent(agentId: string): void {
|
|
1458
|
+
agents = agents.filter((a) => a.id !== agentId)
|
|
1459
|
+
balancers.forEach((b) => b.removeAgent(agentId))
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function getAgents(): AgentInfo[] {
|
|
1463
|
+
return [...agents]
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return { route, addAgent, removeAgent, getAgents }
|
|
1467
|
+
}
|