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