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.
- package/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +134 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +1 -0
- 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 +666 -0
- package/dist/agent-comms.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/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/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/index.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 +905 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +14 -13
- package/src/actions.ts +9 -8
- package/src/agent-comms.ts +1238 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/error-escalation.ts +1135 -0
- package/src/index.ts +223 -0
- package/src/load-balancing.ts +1381 -0
- package/src/types.ts +8 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- 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
|
+
}
|