digital-workers 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +14 -0
  3. package/LICENSE +21 -0
  4. package/README.md +134 -180
  5. package/dist/actions.d.ts.map +1 -1
  6. package/dist/actions.js +1 -0
  7. package/dist/actions.js.map +1 -1
  8. package/dist/agent-comms.d.ts +438 -0
  9. package/dist/agent-comms.d.ts.map +1 -0
  10. package/dist/agent-comms.js +666 -0
  11. package/dist/agent-comms.js.map +1 -0
  12. package/dist/capability-tiers.d.ts +230 -0
  13. package/dist/capability-tiers.d.ts.map +1 -0
  14. package/dist/capability-tiers.js +388 -0
  15. package/dist/capability-tiers.js.map +1 -0
  16. package/dist/cascade-context.d.ts +523 -0
  17. package/dist/cascade-context.d.ts.map +1 -0
  18. package/dist/cascade-context.js +494 -0
  19. package/dist/cascade-context.js.map +1 -0
  20. package/dist/error-escalation.d.ts +416 -0
  21. package/dist/error-escalation.d.ts.map +1 -0
  22. package/dist/error-escalation.js +656 -0
  23. package/dist/error-escalation.js.map +1 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +34 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/load-balancing.d.ts +395 -0
  29. package/dist/load-balancing.d.ts.map +1 -0
  30. package/dist/load-balancing.js +905 -0
  31. package/dist/load-balancing.js.map +1 -0
  32. package/dist/types.d.ts +8 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/types.js +1 -0
  35. package/dist/types.js.map +1 -1
  36. package/package.json +14 -13
  37. package/src/actions.ts +9 -8
  38. package/src/agent-comms.ts +1238 -0
  39. package/src/capability-tiers.ts +545 -0
  40. package/src/cascade-context.ts +648 -0
  41. package/src/error-escalation.ts +1135 -0
  42. package/src/index.ts +223 -0
  43. package/src/load-balancing.ts +1381 -0
  44. package/src/types.ts +8 -0
  45. package/test/agent-comms.test.ts +1397 -0
  46. package/test/capability-tiers.test.ts +631 -0
  47. package/test/cascade-context.test.ts +692 -0
  48. package/test/error-escalation.test.ts +1205 -0
  49. package/test/load-balancing-thread-safety.test.ts +464 -0
  50. package/test/load-balancing.test.ts +1145 -0
  51. package/test/types.test.ts +35 -0
@@ -0,0 +1,1238 @@
1
+ /**
2
+ * Agent-to-Agent Communication Layer
3
+ *
4
+ * Provides structured messaging, coordination patterns, and handoff protocols
5
+ * for communication between agents in the digital-workers system.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { Worker, WorkerRef } from './types.js'
11
+
12
+ // =============================================================================
13
+ // Type Definitions
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Message type for agent-to-agent communication
18
+ */
19
+ export type MessageType =
20
+ | 'request'
21
+ | 'response'
22
+ | 'notification'
23
+ | 'handoff'
24
+ | 'ack'
25
+ | 'error'
26
+
27
+ /**
28
+ * Message priority levels
29
+ */
30
+ export type MessagePriority = 'low' | 'normal' | 'high' | 'urgent'
31
+
32
+ /**
33
+ * Agent message for inter-agent communication
34
+ */
35
+ export interface AgentMessage<T = unknown> {
36
+ /** Unique message identifier */
37
+ id: string
38
+ /** Message type */
39
+ type: MessageType
40
+ /** Sender agent ID */
41
+ sender: string
42
+ /** Recipient agent ID */
43
+ recipient: string
44
+ /** Message payload */
45
+ payload: T
46
+ /** Message timestamp */
47
+ timestamp: Date
48
+ /** Correlation ID for request/response pairing */
49
+ correlationId?: string
50
+ /** Reply-to agent ID */
51
+ replyTo?: string
52
+ /** Time-to-live in milliseconds */
53
+ ttl?: number
54
+ /** Message priority */
55
+ priority?: MessagePriority
56
+ /** Additional metadata */
57
+ metadata?: Record<string, unknown>
58
+ }
59
+
60
+ /**
61
+ * Message delivery status
62
+ */
63
+ export type DeliveryStatus =
64
+ | 'pending'
65
+ | 'delivered'
66
+ | 'acknowledged'
67
+ | 'failed'
68
+ | 'expired'
69
+
70
+ /**
71
+ * Message envelope with delivery metadata
72
+ */
73
+ export interface MessageEnvelope<T = unknown> {
74
+ /** The wrapped message */
75
+ message: AgentMessage<T>
76
+ /** Number of delivery attempts */
77
+ deliveryAttempts: number
78
+ /** Timestamp of first delivery attempt */
79
+ firstAttemptAt: Date
80
+ /** Timestamp of last delivery attempt */
81
+ lastAttemptAt: Date
82
+ /** Current delivery status */
83
+ status: DeliveryStatus
84
+ /** Error message if failed */
85
+ error?: string
86
+ }
87
+
88
+ /**
89
+ * Message acknowledgment
90
+ */
91
+ export interface MessageAck {
92
+ /** ID of acknowledged message */
93
+ messageId: string
94
+ /** Acknowledgment status */
95
+ status: 'received' | 'processed' | 'failed'
96
+ /** Acknowledgment timestamp */
97
+ timestamp: Date
98
+ /** Acknowledging agent ID */
99
+ agentId: string
100
+ /** Processing result (for 'processed' status) */
101
+ result?: unknown
102
+ /** Error details (for 'failed' status) */
103
+ error?: string
104
+ }
105
+
106
+ /**
107
+ * Handoff request for transferring work between agents
108
+ */
109
+ export interface HandoffRequest {
110
+ /** Unique handoff identifier */
111
+ id: string
112
+ /** Source agent ID */
113
+ fromAgent: string
114
+ /** Target agent ID */
115
+ toAgent: string
116
+ /** Context to transfer */
117
+ context: Record<string, unknown>
118
+ /** Reason for handoff */
119
+ reason?: string
120
+ /** Handoff priority */
121
+ priority?: MessagePriority
122
+ /** Request timestamp */
123
+ timestamp: Date
124
+ /** Timeout in milliseconds */
125
+ timeout?: number
126
+ /** Previous handoff attempt ID (for retries) */
127
+ previousAttempt?: string
128
+ /** Callback on timeout */
129
+ onTimeout?: (msg: AgentMessage) => void
130
+ }
131
+
132
+ /**
133
+ * Handoff status
134
+ */
135
+ export type HandoffStatus =
136
+ | 'pending'
137
+ | 'accepted'
138
+ | 'rejected'
139
+ | 'completed'
140
+ | 'expired'
141
+ | 'failed'
142
+
143
+ /**
144
+ * Handoff result
145
+ */
146
+ export interface HandoffResult {
147
+ /** Handoff ID */
148
+ handoffId: string
149
+ /** Current status */
150
+ status: HandoffStatus
151
+ /** Result data (for completed handoffs) */
152
+ result?: unknown
153
+ /** Rejection reason */
154
+ reason?: string
155
+ /** Completed timestamp */
156
+ completedAt?: Date
157
+ }
158
+
159
+ /**
160
+ * Coordination pattern types
161
+ */
162
+ export type CoordinationPattern =
163
+ | 'request-response'
164
+ | 'fan-out'
165
+ | 'fan-in'
166
+ | 'pipeline'
167
+ | 'publish-subscribe'
168
+
169
+ // =============================================================================
170
+ // Message Handler Types
171
+ // =============================================================================
172
+
173
+ /**
174
+ * Message handler function type
175
+ */
176
+ export type MessageHandler<T = unknown> = (
177
+ message: AgentMessage<T>
178
+ ) => void | Promise<void>
179
+
180
+ /**
181
+ * Subscription options
182
+ */
183
+ export interface SubscribeOptions {
184
+ /** Filter by message types */
185
+ types?: MessageType[]
186
+ /** Filter by sender */
187
+ from?: string
188
+ }
189
+
190
+ // =============================================================================
191
+ // AgentMessageBus Configuration
192
+ // =============================================================================
193
+
194
+ /**
195
+ * Message bus configuration options
196
+ */
197
+ export interface MessageBusOptions {
198
+ /** Enable message persistence */
199
+ persistence?: boolean
200
+ /** Default message TTL in milliseconds */
201
+ defaultTtl?: number
202
+ /** Maximum queue size per agent */
203
+ maxQueueSize?: number
204
+ }
205
+
206
+ // =============================================================================
207
+ // Internal Types
208
+ // =============================================================================
209
+
210
+ interface Subscription {
211
+ handler: MessageHandler
212
+ options?: SubscribeOptions
213
+ }
214
+
215
+ interface HandoffState {
216
+ request: HandoffRequest
217
+ status: HandoffStatus
218
+ previousAttempts: string[]
219
+ timeoutId?: ReturnType<typeof setTimeout>
220
+ /** The original handoff message for tracking */
221
+ originalMessage?: AgentMessage
222
+ }
223
+
224
+ interface StoredMessage {
225
+ envelope: MessageEnvelope
226
+ storedAt: Date
227
+ }
228
+
229
+ // =============================================================================
230
+ // AgentMessageBus Implementation
231
+ // =============================================================================
232
+
233
+ /**
234
+ * Agent message bus for routing messages between agents
235
+ */
236
+ export class AgentMessageBus {
237
+ private subscriptions = new Map<string, Subscription[]>()
238
+ private pendingAcks = new Map<string, Set<string>>()
239
+ private messageStatus = new Map<string, DeliveryStatus>()
240
+ private handoffs = new Map<string, HandoffState>()
241
+ private storedMessages: StoredMessage[] = []
242
+ private messageQueue = new Map<string, AgentMessage[]>()
243
+ private processingAgent = new Set<string>()
244
+ private disposed = false
245
+ private options: Required<MessageBusOptions>
246
+
247
+ constructor(options: MessageBusOptions = {}) {
248
+ this.options = {
249
+ persistence: options.persistence ?? false,
250
+ defaultTtl: options.defaultTtl ?? 30000,
251
+ maxQueueSize: options.maxQueueSize ?? 1000,
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Send a message to an agent
257
+ */
258
+ async send<T>(message: AgentMessage<T>): Promise<MessageEnvelope<T>> {
259
+ if (this.disposed) {
260
+ return this.createFailedEnvelope(message, 'Message bus disposed')
261
+ }
262
+
263
+ const envelope: MessageEnvelope<T> = {
264
+ message,
265
+ deliveryAttempts: 1,
266
+ firstAttemptAt: new Date(),
267
+ lastAttemptAt: new Date(),
268
+ status: 'pending',
269
+ }
270
+
271
+ // Store message if persistence is enabled
272
+ if (this.options.persistence) {
273
+ this.storedMessages.push({ envelope: envelope as MessageEnvelope, storedAt: new Date() })
274
+ }
275
+
276
+ // Get subscriptions for recipient
277
+ const subs = this.subscriptions.get(message.recipient)
278
+ if (!subs || subs.length === 0) {
279
+ envelope.status = 'failed'
280
+ envelope.error = `Agent '${message.recipient}' not found`
281
+ this.messageStatus.set(message.id, 'failed')
282
+ return envelope
283
+ }
284
+
285
+ // Track pending acknowledgment
286
+ if (message.type === 'request') {
287
+ const senderAcks = this.pendingAcks.get(message.sender) ?? new Set()
288
+ senderAcks.add(message.id)
289
+ this.pendingAcks.set(message.sender, senderAcks)
290
+ }
291
+
292
+ // Setup TTL expiration
293
+ if (message.ttl) {
294
+ setTimeout(() => {
295
+ if (this.messageStatus.get(message.id) !== 'acknowledged') {
296
+ this.messageStatus.set(message.id, 'expired')
297
+ }
298
+ }, message.ttl)
299
+ }
300
+
301
+ // Deliver to matching subscribers
302
+ try {
303
+ await this.deliverMessage(message, subs)
304
+ envelope.status = 'delivered'
305
+ this.messageStatus.set(message.id, 'delivered')
306
+ } catch (error) {
307
+ envelope.status = 'failed'
308
+ envelope.error = error instanceof Error ? error.message : String(error)
309
+ this.messageStatus.set(message.id, 'failed')
310
+ }
311
+
312
+ return envelope
313
+ }
314
+
315
+ /**
316
+ * Deliver message to subscribers with queue handling
317
+ */
318
+ private async deliverMessage<T>(
319
+ message: AgentMessage<T>,
320
+ subs: Subscription[]
321
+ ): Promise<void> {
322
+ const matchingSubs = subs.filter((sub) => this.matchesFilter(message, sub.options))
323
+
324
+ if (matchingSubs.length === 0) {
325
+ return
326
+ }
327
+
328
+ // Queue messages if agent is busy
329
+ if (this.processingAgent.has(message.recipient)) {
330
+ const queue = this.messageQueue.get(message.recipient) ?? []
331
+ queue.push(message as AgentMessage)
332
+ this.messageQueue.set(message.recipient, queue)
333
+ return
334
+ }
335
+
336
+ this.processingAgent.add(message.recipient)
337
+
338
+ try {
339
+ await Promise.all(matchingSubs.map((sub) => sub.handler(message as AgentMessage)))
340
+ } finally {
341
+ this.processingAgent.delete(message.recipient)
342
+ await this.processQueue(message.recipient, subs)
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Process queued messages
348
+ */
349
+ private async processQueue(agentId: string, subs: Subscription[]): Promise<void> {
350
+ const queue = this.messageQueue.get(agentId)
351
+ if (!queue || queue.length === 0) return
352
+
353
+ const nextMessage = queue.shift()!
354
+ this.messageQueue.set(agentId, queue)
355
+
356
+ await this.deliverMessage(nextMessage, subs)
357
+ }
358
+
359
+ /**
360
+ * Check if message matches subscription filter
361
+ */
362
+ private matchesFilter(message: AgentMessage, options?: SubscribeOptions): boolean {
363
+ if (!options) return true
364
+ if (options.types && !options.types.includes(message.type)) return false
365
+ if (options.from && message.sender !== options.from) return false
366
+ return true
367
+ }
368
+
369
+ /**
370
+ * Subscribe to messages for an agent
371
+ */
372
+ subscribe(
373
+ agentId: string,
374
+ handler: MessageHandler,
375
+ options?: SubscribeOptions
376
+ ): () => void {
377
+ const subs = this.subscriptions.get(agentId) ?? []
378
+ const subscription: Subscription = { handler, options }
379
+ subs.push(subscription)
380
+ this.subscriptions.set(agentId, subs)
381
+
382
+ return () => {
383
+ const currentSubs = this.subscriptions.get(agentId) ?? []
384
+ const index = currentSubs.indexOf(subscription)
385
+ if (index !== -1) {
386
+ currentSubs.splice(index, 1)
387
+ this.subscriptions.set(agentId, currentSubs)
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Acknowledge a message
394
+ */
395
+ async acknowledge(
396
+ messageId: string,
397
+ agentId: string,
398
+ status: 'received' | 'processed' | 'failed'
399
+ ): Promise<void> {
400
+ this.messageStatus.set(messageId, 'acknowledged')
401
+
402
+ // Remove from pending acks
403
+ for (const entry of Array.from(this.pendingAcks.entries())) {
404
+ const [sender, acks] = entry
405
+ if (acks.has(messageId)) {
406
+ acks.delete(messageId)
407
+ this.pendingAcks.set(sender, acks)
408
+ break
409
+ }
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Get pending acknowledgments for a sender
415
+ */
416
+ getPendingAcks(senderId: string): string[] {
417
+ const acks = this.pendingAcks.get(senderId)
418
+ return acks ? Array.from(acks) : []
419
+ }
420
+
421
+ /**
422
+ * Get message delivery status
423
+ */
424
+ getMessageStatus(messageId: string): DeliveryStatus | undefined {
425
+ return this.messageStatus.get(messageId)
426
+ }
427
+
428
+ /**
429
+ * Get handoff status
430
+ */
431
+ getHandoffStatus(handoffId: string): HandoffStatus | undefined {
432
+ return this.handoffs.get(handoffId)?.status
433
+ }
434
+
435
+ /**
436
+ * Get handoff history (previous attempts)
437
+ */
438
+ getHandoffHistory(handoffId: string): string[] {
439
+ return this.handoffs.get(handoffId)?.previousAttempts ?? []
440
+ }
441
+
442
+ /**
443
+ * Register a handoff
444
+ */
445
+ registerHandoff(request: HandoffRequest, originalMessage?: AgentMessage): void {
446
+ const state: HandoffState = {
447
+ request,
448
+ status: 'pending',
449
+ previousAttempts: request.previousAttempt ? [request.previousAttempt] : [],
450
+ originalMessage,
451
+ }
452
+
453
+ // Setup timeout if specified
454
+ if (request.timeout) {
455
+ state.timeoutId = setTimeout(() => {
456
+ const currentState = this.handoffs.get(request.id)
457
+ if (currentState && currentState.status === 'pending') {
458
+ currentState.status = 'expired'
459
+ this.handoffs.set(request.id, currentState)
460
+
461
+ // Notify initiator of timeout
462
+ if (request.onTimeout) {
463
+ const timeoutMsg: AgentMessage = {
464
+ id: `timeout_${request.id}`,
465
+ type: 'handoff',
466
+ sender: 'system',
467
+ recipient: request.fromAgent,
468
+ payload: { action: 'timeout', handoffId: request.id },
469
+ timestamp: new Date(),
470
+ }
471
+ request.onTimeout(timeoutMsg)
472
+ }
473
+ }
474
+ }, request.timeout)
475
+ }
476
+
477
+ this.handoffs.set(request.id, state)
478
+ }
479
+
480
+ /**
481
+ * Get handoff request info
482
+ */
483
+ getHandoffRequest(handoffId: string): HandoffRequest | undefined {
484
+ return this.handoffs.get(handoffId)?.request
485
+ }
486
+
487
+ /**
488
+ * Update handoff status
489
+ */
490
+ updateHandoffStatus(handoffId: string, status: HandoffStatus): void {
491
+ const state = this.handoffs.get(handoffId)
492
+ if (state) {
493
+ // Clear timeout if pending timeout exists
494
+ if (state.timeoutId) {
495
+ clearTimeout(state.timeoutId)
496
+ }
497
+ state.status = status
498
+ this.handoffs.set(handoffId, state)
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Get stored messages (for persistence)
504
+ */
505
+ getStoredMessages(): MessageEnvelope[] {
506
+ return this.storedMessages.map((s) => s.envelope)
507
+ }
508
+
509
+ /**
510
+ * Get message history for an agent
511
+ */
512
+ getMessageHistory(
513
+ agentId: string,
514
+ options?: { limit?: number; from?: Date; to?: Date }
515
+ ): MessageEnvelope[] {
516
+ let messages = this.storedMessages
517
+ .filter(
518
+ (s) =>
519
+ s.envelope.message.recipient === agentId ||
520
+ s.envelope.message.sender === agentId
521
+ )
522
+ .map((s) => s.envelope)
523
+
524
+ if (options?.from) {
525
+ messages = messages.filter(
526
+ (m) => m.message.timestamp >= options.from!
527
+ )
528
+ }
529
+
530
+ if (options?.to) {
531
+ messages = messages.filter((m) => m.message.timestamp <= options.to!)
532
+ }
533
+
534
+ if (options?.limit) {
535
+ messages = messages.slice(-options.limit)
536
+ }
537
+
538
+ return messages
539
+ }
540
+
541
+ /**
542
+ * Clear old messages
543
+ */
544
+ clearMessages(options?: { olderThan?: Date }): void {
545
+ if (options?.olderThan) {
546
+ this.storedMessages = this.storedMessages.filter(
547
+ (s) => s.storedAt >= options.olderThan!
548
+ )
549
+ } else {
550
+ this.storedMessages = []
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Dispose the message bus
556
+ */
557
+ dispose(): void {
558
+ this.disposed = true
559
+ this.subscriptions.clear()
560
+ this.pendingAcks.clear()
561
+ this.messageStatus.clear()
562
+ this.messageQueue.clear()
563
+ this.processingAgent.clear()
564
+
565
+ // Clear all handoff timeouts
566
+ for (const state of Array.from(this.handoffs.values())) {
567
+ if (state.timeoutId) {
568
+ clearTimeout(state.timeoutId)
569
+ }
570
+ }
571
+ this.handoffs.clear()
572
+ }
573
+
574
+ /**
575
+ * Create a failed envelope
576
+ */
577
+ private createFailedEnvelope<T>(
578
+ message: AgentMessage<T>,
579
+ error: string
580
+ ): MessageEnvelope<T> {
581
+ return {
582
+ message,
583
+ deliveryAttempts: 0,
584
+ firstAttemptAt: new Date(),
585
+ lastAttemptAt: new Date(),
586
+ status: 'failed',
587
+ error,
588
+ }
589
+ }
590
+ }
591
+
592
+ // =============================================================================
593
+ // Factory Function
594
+ // =============================================================================
595
+
596
+ /**
597
+ * Create a new message bus instance
598
+ */
599
+ export function createMessageBus(options?: MessageBusOptions): AgentMessageBus {
600
+ return new AgentMessageBus(options)
601
+ }
602
+
603
+ // =============================================================================
604
+ // Helper Functions
605
+ // =============================================================================
606
+
607
+ /**
608
+ * Generate a unique message ID
609
+ */
610
+ function generateMessageId(): string {
611
+ return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
612
+ }
613
+
614
+ /**
615
+ * Generate a unique handoff ID
616
+ */
617
+ function generateHandoffId(): string {
618
+ return `handoff_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
619
+ }
620
+
621
+ /**
622
+ * Generate a unique correlation ID
623
+ */
624
+ function generateCorrelationId(): string {
625
+ return `corr_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
626
+ }
627
+
628
+ /**
629
+ * Resolve agent ID from Worker, WorkerRef, or string
630
+ */
631
+ function resolveAgentId(agent: Worker | WorkerRef | string): string {
632
+ if (typeof agent === 'string') return agent
633
+ return agent.id
634
+ }
635
+
636
+ // =============================================================================
637
+ // Core Communication Functions
638
+ // =============================================================================
639
+
640
+ /**
641
+ * Message send options
642
+ */
643
+ export interface SendOptions {
644
+ /** Message priority */
645
+ priority?: MessagePriority
646
+ /** Time-to-live in milliseconds */
647
+ ttl?: number
648
+ /** Correlation ID for request/response pairing */
649
+ correlationId?: string
650
+ /** Additional metadata */
651
+ metadata?: Record<string, unknown>
652
+ }
653
+
654
+ /**
655
+ * Send a message to a specific agent
656
+ */
657
+ export async function sendToAgent<T>(
658
+ bus: AgentMessageBus,
659
+ sender: Worker | WorkerRef | string,
660
+ recipient: Worker | WorkerRef | string,
661
+ payload: T,
662
+ options?: SendOptions
663
+ ): Promise<MessageEnvelope<T>> {
664
+ const message: AgentMessage<T> = {
665
+ id: generateMessageId(),
666
+ type: 'notification',
667
+ sender: resolveAgentId(sender),
668
+ recipient: resolveAgentId(recipient),
669
+ payload,
670
+ timestamp: new Date(),
671
+ priority: options?.priority,
672
+ ttl: options?.ttl,
673
+ correlationId: options?.correlationId,
674
+ metadata: options?.metadata,
675
+ }
676
+
677
+ return bus.send(message)
678
+ }
679
+
680
+ /**
681
+ * Broadcast a message to multiple agents
682
+ */
683
+ export async function broadcastToGroup<T>(
684
+ bus: AgentMessageBus,
685
+ sender: Worker | WorkerRef | string,
686
+ recipients: (Worker | WorkerRef | string)[],
687
+ payload: T,
688
+ options?: SendOptions
689
+ ): Promise<MessageEnvelope<T>[]> {
690
+ const correlationId = options?.correlationId ?? generateCorrelationId()
691
+
692
+ const results = await Promise.all(
693
+ recipients.map((recipient) =>
694
+ sendToAgent(bus, sender, recipient, payload, {
695
+ ...options,
696
+ correlationId,
697
+ })
698
+ )
699
+ )
700
+
701
+ return results
702
+ }
703
+
704
+ /**
705
+ * Request options
706
+ */
707
+ export interface RequestOptions extends SendOptions {
708
+ /** Request timeout in milliseconds */
709
+ timeout?: number
710
+ }
711
+
712
+ /**
713
+ * Send a request to an agent and await response
714
+ */
715
+ export async function requestFromAgent<TReq, TRes>(
716
+ bus: AgentMessageBus,
717
+ sender: Worker | WorkerRef | string,
718
+ recipient: Worker | WorkerRef | string,
719
+ payload: TReq,
720
+ options?: RequestOptions
721
+ ): Promise<AgentMessage<TRes>> {
722
+ const senderId = resolveAgentId(sender)
723
+ const recipientId = resolveAgentId(recipient)
724
+ const messageId = generateMessageId()
725
+ const timeout = options?.timeout ?? 30000
726
+
727
+ return new Promise<AgentMessage<TRes>>((resolve, reject) => {
728
+ let timeoutId: ReturnType<typeof setTimeout>
729
+ let unsubscribe: (() => void) | undefined
730
+
731
+ // Setup response handler
732
+ unsubscribe = bus.subscribe(senderId, (response) => {
733
+ if (response.correlationId === messageId) {
734
+ clearTimeout(timeoutId)
735
+ unsubscribe?.()
736
+
737
+ if (response.type === 'error') {
738
+ const errorPayload = response.payload as { error?: string }
739
+ reject(new Error(errorPayload.error ?? 'Request failed'))
740
+ } else {
741
+ resolve(response as AgentMessage<TRes>)
742
+ }
743
+ }
744
+ })
745
+
746
+ // Setup timeout
747
+ timeoutId = setTimeout(() => {
748
+ unsubscribe?.()
749
+ reject(new Error('Request timeout'))
750
+ }, timeout)
751
+
752
+ // Send request
753
+ const message: AgentMessage<TReq> = {
754
+ id: messageId,
755
+ type: 'request',
756
+ sender: senderId,
757
+ recipient: recipientId,
758
+ payload,
759
+ timestamp: new Date(),
760
+ correlationId: messageId,
761
+ replyTo: senderId,
762
+ priority: options?.priority,
763
+ ttl: options?.ttl,
764
+ metadata: options?.metadata,
765
+ }
766
+
767
+ bus.send(message).then((envelope) => {
768
+ // Fail fast if delivery failed (recipient not found, etc.)
769
+ if (envelope.status === 'failed') {
770
+ clearTimeout(timeoutId)
771
+ unsubscribe?.()
772
+ reject(new Error(envelope.error ?? 'Message delivery failed'))
773
+ }
774
+ }).catch((error) => {
775
+ clearTimeout(timeoutId)
776
+ unsubscribe?.()
777
+ reject(error)
778
+ })
779
+ })
780
+ }
781
+
782
+ /**
783
+ * On-message handler options
784
+ */
785
+ export interface OnMessageOptions {
786
+ /** Filter by sender */
787
+ from?: string
788
+ /** Filter by message types */
789
+ types?: MessageType[]
790
+ }
791
+
792
+ /**
793
+ * Register a message handler for an agent
794
+ */
795
+ export function onMessage<T = unknown>(
796
+ bus: AgentMessageBus,
797
+ agentId: string,
798
+ handler: MessageHandler<T>,
799
+ options?: OnMessageOptions
800
+ ): () => void {
801
+ return bus.subscribe(agentId, handler as MessageHandler, {
802
+ from: options?.from,
803
+ types: options?.types,
804
+ })
805
+ }
806
+
807
+ /**
808
+ * Send an acknowledgment for a received message
809
+ */
810
+ export async function acknowledge(
811
+ bus: AgentMessageBus,
812
+ message: AgentMessage,
813
+ status: 'received' | 'processed',
814
+ result?: unknown
815
+ ): Promise<void> {
816
+ // Send ack message to original sender
817
+ const ackMessage: AgentMessage = {
818
+ id: generateMessageId(),
819
+ type: 'ack',
820
+ sender: message.recipient,
821
+ recipient: message.sender,
822
+ payload: {
823
+ messageId: message.id,
824
+ status,
825
+ timestamp: new Date(),
826
+ agentId: message.recipient,
827
+ result,
828
+ },
829
+ timestamp: new Date(),
830
+ correlationId: message.id,
831
+ }
832
+
833
+ await bus.send(ackMessage)
834
+ await bus.acknowledge(message.id, message.recipient, status)
835
+ }
836
+
837
+ // =============================================================================
838
+ // Coordination Patterns
839
+ // =============================================================================
840
+
841
+ /**
842
+ * Request-response pattern options
843
+ */
844
+ export interface RequestResponseOptions<T> {
845
+ /** Requesting agent */
846
+ from: Worker | WorkerRef | string
847
+ /** Target agent */
848
+ to: Worker | WorkerRef | string
849
+ /** Request payload */
850
+ payload: T
851
+ /** Timeout in milliseconds */
852
+ timeout?: number
853
+ /** Message priority */
854
+ priority?: MessagePriority
855
+ }
856
+
857
+ /**
858
+ * Execute request-response pattern
859
+ */
860
+ export async function requestResponse<TReq, TRes>(
861
+ bus: AgentMessageBus,
862
+ options: RequestResponseOptions<TReq>
863
+ ): Promise<AgentMessage<TRes>> {
864
+ return requestFromAgent<TReq, TRes>(bus, options.from, options.to, options.payload, {
865
+ timeout: options.timeout,
866
+ priority: options.priority,
867
+ })
868
+ }
869
+
870
+ /**
871
+ * Fan-out pattern options
872
+ */
873
+ export interface FanOutOptions<T> {
874
+ /** Coordinating agent */
875
+ from: Worker | WorkerRef | string
876
+ /** Target agents */
877
+ to: (Worker | WorkerRef | string)[]
878
+ /** Payload to distribute */
879
+ payload: T
880
+ /** Timeout per agent */
881
+ timeout?: number
882
+ /** Continue even if some agents fail */
883
+ continueOnError?: boolean
884
+ }
885
+
886
+ /**
887
+ * Fan-out response result
888
+ */
889
+ export interface FanOutResult<T> {
890
+ /** Target agent ID */
891
+ agentId: string
892
+ /** Whether the request succeeded */
893
+ success: boolean
894
+ /** Response payload if successful */
895
+ payload?: T
896
+ /** Error if failed */
897
+ error?: string
898
+ }
899
+
900
+ /**
901
+ * Execute fan-out pattern - distribute work to multiple agents
902
+ */
903
+ export async function fanOut<TReq, TRes>(
904
+ bus: AgentMessageBus,
905
+ options: FanOutOptions<TReq>
906
+ ): Promise<FanOutResult<TRes>[]> {
907
+ const correlationId = generateCorrelationId()
908
+ const timeout = options.timeout ?? 30000
909
+ const fromId = resolveAgentId(options.from)
910
+
911
+ const results = await Promise.all(
912
+ options.to.map(async (agent): Promise<FanOutResult<TRes>> => {
913
+ const agentId = resolveAgentId(agent)
914
+
915
+ try {
916
+ const response = await requestFromAgent<TReq, TRes>(
917
+ bus,
918
+ fromId,
919
+ agentId,
920
+ options.payload,
921
+ { timeout, correlationId }
922
+ )
923
+
924
+ return {
925
+ agentId,
926
+ success: true,
927
+ payload: response.payload,
928
+ }
929
+ } catch (error) {
930
+ if (!options.continueOnError) {
931
+ throw error
932
+ }
933
+
934
+ return {
935
+ agentId,
936
+ success: false,
937
+ error: error instanceof Error ? error.message : String(error),
938
+ }
939
+ }
940
+ })
941
+ )
942
+
943
+ return results
944
+ }
945
+
946
+ /**
947
+ * Fan-in pattern options
948
+ */
949
+ export interface FanInOptions<T> {
950
+ /** Collecting agent */
951
+ collector: Worker | WorkerRef | string
952
+ /** Source agents to collect from */
953
+ sources: (Worker | WorkerRef | string)[]
954
+ /** Timeout for all sources */
955
+ timeout?: number
956
+ /** Handler to get data from each source */
957
+ onSourceMessage: (sourceId: string) => Promise<T>
958
+ }
959
+
960
+ /**
961
+ * Execute fan-in pattern - collect responses from multiple agents
962
+ */
963
+ export async function fanIn<T>(
964
+ bus: AgentMessageBus,
965
+ options: FanInOptions<T>
966
+ ): Promise<T[]> {
967
+ const results = await Promise.all(
968
+ options.sources.map((source) => {
969
+ const sourceId = resolveAgentId(source)
970
+ return options.onSourceMessage(sourceId)
971
+ })
972
+ )
973
+
974
+ return results
975
+ }
976
+
977
+ /**
978
+ * Pipeline pattern options
979
+ */
980
+ export interface PipelineOptions<T> {
981
+ /** Orchestrating agent */
982
+ initiator: Worker | WorkerRef | string
983
+ /** Ordered list of pipeline stages (agent IDs) */
984
+ stages: (Worker | WorkerRef | string)[]
985
+ /** Initial input */
986
+ input: T
987
+ /** Timeout per stage */
988
+ stageTimeout?: number
989
+ }
990
+
991
+ /**
992
+ * Execute pipeline pattern - chain agents in sequence
993
+ */
994
+ export async function pipeline<T>(
995
+ bus: AgentMessageBus,
996
+ options: PipelineOptions<T>
997
+ ): Promise<AgentMessage<T>> {
998
+ const initiatorId = resolveAgentId(options.initiator)
999
+ let currentPayload = options.input
1000
+ let lastResponse: AgentMessage<T> | undefined
1001
+
1002
+ for (const stage of options.stages) {
1003
+ const stageId = resolveAgentId(stage)
1004
+
1005
+ lastResponse = await requestFromAgent<T, T>(
1006
+ bus,
1007
+ initiatorId,
1008
+ stageId,
1009
+ currentPayload,
1010
+ { timeout: options.stageTimeout }
1011
+ )
1012
+
1013
+ currentPayload = lastResponse.payload
1014
+ }
1015
+
1016
+ return lastResponse!
1017
+ }
1018
+
1019
+ // =============================================================================
1020
+ // Handoff Protocol
1021
+ // =============================================================================
1022
+
1023
+ /**
1024
+ * Initiate handoff options
1025
+ */
1026
+ export interface InitiateHandoffOptions {
1027
+ /** Source agent */
1028
+ fromAgent: Worker | WorkerRef | string
1029
+ /** Target agent */
1030
+ toAgent: Worker | WorkerRef | string
1031
+ /** Context to transfer */
1032
+ context: Record<string, unknown>
1033
+ /** Reason for handoff */
1034
+ reason?: string
1035
+ /** Timeout in milliseconds */
1036
+ timeout?: number
1037
+ /** Previous handoff attempt ID */
1038
+ previousAttempt?: string
1039
+ /** Callback on timeout */
1040
+ onTimeout?: (msg: AgentMessage) => void
1041
+ }
1042
+
1043
+ /**
1044
+ * Initiate a handoff to another agent
1045
+ */
1046
+ export async function initiateHandoff(
1047
+ bus: AgentMessageBus,
1048
+ options: InitiateHandoffOptions
1049
+ ): Promise<{ handoffId: string; status: HandoffStatus }> {
1050
+ const handoffId = generateHandoffId()
1051
+ const fromAgentId = resolveAgentId(options.fromAgent)
1052
+ const toAgentId = resolveAgentId(options.toAgent)
1053
+
1054
+ const request: HandoffRequest = {
1055
+ id: handoffId,
1056
+ fromAgent: fromAgentId,
1057
+ toAgent: toAgentId,
1058
+ context: options.context,
1059
+ reason: options.reason,
1060
+ timestamp: new Date(),
1061
+ timeout: options.timeout,
1062
+ previousAttempt: options.previousAttempt,
1063
+ onTimeout: options.onTimeout,
1064
+ }
1065
+
1066
+ // Register handoff in bus
1067
+ bus.registerHandoff(request)
1068
+
1069
+ // Send handoff request message
1070
+ const message: AgentMessage = {
1071
+ id: generateMessageId(),
1072
+ type: 'handoff',
1073
+ sender: fromAgentId,
1074
+ recipient: toAgentId,
1075
+ payload: {
1076
+ action: 'request',
1077
+ handoffId,
1078
+ context: options.context,
1079
+ reason: options.reason,
1080
+ },
1081
+ timestamp: new Date(),
1082
+ correlationId: handoffId,
1083
+ }
1084
+
1085
+ await bus.send(message)
1086
+
1087
+ return { handoffId, status: 'pending' }
1088
+ }
1089
+
1090
+ /**
1091
+ * Accept a pending handoff
1092
+ */
1093
+ export async function acceptHandoff(
1094
+ bus: AgentMessageBus,
1095
+ handoffId: string,
1096
+ agentId: string
1097
+ ): Promise<HandoffResult> {
1098
+ const status = bus.getHandoffStatus(handoffId)
1099
+
1100
+ if (!status) {
1101
+ throw new Error('Handoff not found')
1102
+ }
1103
+
1104
+ if (status !== 'pending') {
1105
+ throw new Error(`Cannot accept handoff in '${status}' state`)
1106
+ }
1107
+
1108
+ bus.updateHandoffStatus(handoffId, 'accepted')
1109
+
1110
+ // Get the handoff request to find the initiating agent
1111
+ const request = bus.getHandoffRequest(handoffId)
1112
+
1113
+ if (request) {
1114
+ const acceptMessage: AgentMessage = {
1115
+ id: generateMessageId(),
1116
+ type: 'handoff',
1117
+ sender: agentId,
1118
+ recipient: request.fromAgent,
1119
+ payload: {
1120
+ action: 'accepted',
1121
+ handoffId,
1122
+ },
1123
+ timestamp: new Date(),
1124
+ correlationId: handoffId,
1125
+ }
1126
+
1127
+ await bus.send(acceptMessage)
1128
+ }
1129
+
1130
+ return { handoffId, status: 'accepted' }
1131
+ }
1132
+
1133
+ /**
1134
+ * Reject handoff options
1135
+ */
1136
+ export interface RejectHandoffOptions {
1137
+ /** Reason for rejection */
1138
+ reason?: string
1139
+ }
1140
+
1141
+ /**
1142
+ * Reject a pending handoff
1143
+ */
1144
+ export async function rejectHandoff(
1145
+ bus: AgentMessageBus,
1146
+ handoffId: string,
1147
+ agentId: string,
1148
+ options?: RejectHandoffOptions
1149
+ ): Promise<HandoffResult> {
1150
+ const status = bus.getHandoffStatus(handoffId)
1151
+
1152
+ if (!status) {
1153
+ throw new Error('Handoff not found')
1154
+ }
1155
+
1156
+ bus.updateHandoffStatus(handoffId, 'rejected')
1157
+
1158
+ // Get the handoff request to find the initiating agent
1159
+ const request = bus.getHandoffRequest(handoffId)
1160
+
1161
+ if (request) {
1162
+ const rejectMessage: AgentMessage = {
1163
+ id: generateMessageId(),
1164
+ type: 'handoff',
1165
+ sender: agentId,
1166
+ recipient: request.fromAgent,
1167
+ payload: {
1168
+ action: 'rejected',
1169
+ handoffId,
1170
+ reason: options?.reason,
1171
+ },
1172
+ timestamp: new Date(),
1173
+ correlationId: handoffId,
1174
+ }
1175
+
1176
+ await bus.send(rejectMessage)
1177
+ }
1178
+
1179
+ return { handoffId, status: 'rejected', reason: options?.reason }
1180
+ }
1181
+
1182
+ /**
1183
+ * Complete handoff options
1184
+ */
1185
+ export interface CompleteHandoffOptions {
1186
+ /** Result of the handoff work */
1187
+ result?: unknown
1188
+ }
1189
+
1190
+ /**
1191
+ * Complete a handoff (mark work as done)
1192
+ */
1193
+ export async function completeHandoff(
1194
+ bus: AgentMessageBus,
1195
+ handoffId: string,
1196
+ agentId: string,
1197
+ options?: CompleteHandoffOptions
1198
+ ): Promise<HandoffResult> {
1199
+ const status = bus.getHandoffStatus(handoffId)
1200
+
1201
+ if (!status) {
1202
+ throw new Error('Handoff not found')
1203
+ }
1204
+
1205
+ if (status !== 'accepted') {
1206
+ throw new Error('Handoff not accepted')
1207
+ }
1208
+
1209
+ bus.updateHandoffStatus(handoffId, 'completed')
1210
+
1211
+ // Get the handoff request to find the initiating agent
1212
+ const request = bus.getHandoffRequest(handoffId)
1213
+
1214
+ if (request) {
1215
+ const completeMessage: AgentMessage = {
1216
+ id: generateMessageId(),
1217
+ type: 'handoff',
1218
+ sender: agentId,
1219
+ recipient: request.fromAgent,
1220
+ payload: {
1221
+ action: 'completed',
1222
+ handoffId,
1223
+ result: options?.result,
1224
+ },
1225
+ timestamp: new Date(),
1226
+ correlationId: handoffId,
1227
+ }
1228
+
1229
+ await bus.send(completeMessage)
1230
+ }
1231
+
1232
+ return {
1233
+ handoffId,
1234
+ status: 'completed',
1235
+ result: options?.result,
1236
+ completedAt: new Date(),
1237
+ }
1238
+ }