@tjamescouch/agentchat 0.22.0 → 0.22.1

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.
@@ -0,0 +1,391 @@
1
+ /**
2
+ * EscrowHooks - Event system for external escrow integration
3
+ *
4
+ * Allows external systems (blockchain, multi-sig, compliance) to hook into
5
+ * escrow lifecycle events without modifying core AgentChat code.
6
+ *
7
+ * Events:
8
+ * escrow:created - Escrow created when proposal accepted with stakes
9
+ * escrow:released - Escrow released (expired, cancelled)
10
+ * settlement:completion - Proposal completed, stakes returned
11
+ * settlement:dispute - Proposal disputed, stakes transferred/burned
12
+ */
13
+
14
+ import type { Proposal } from './types.js';
15
+
16
+ export const EscrowEvent = {
17
+ CREATED: 'escrow:created',
18
+ RELEASED: 'escrow:released',
19
+ COMPLETION_SETTLED: 'settlement:completion',
20
+ DISPUTE_SETTLED: 'settlement:dispute'
21
+ } as const;
22
+
23
+ export type EscrowEventType = typeof EscrowEvent[keyof typeof EscrowEvent];
24
+
25
+ export interface Logger {
26
+ error?: (message: string, ...args: unknown[]) => void;
27
+ warn?: (message: string, ...args: unknown[]) => void;
28
+ info?: (message: string, ...args: unknown[]) => void;
29
+ debug?: (message: string, ...args: unknown[]) => void;
30
+ }
31
+
32
+ export interface EscrowHooksOptions {
33
+ logger?: Logger;
34
+ continueOnError?: boolean;
35
+ }
36
+
37
+ export interface HandlerResult {
38
+ success: boolean;
39
+ result?: unknown;
40
+ error?: string;
41
+ stack?: string;
42
+ }
43
+
44
+ export interface EmitResult {
45
+ event: EscrowEventType;
46
+ handled: boolean;
47
+ results: HandlerResult[];
48
+ errors?: HandlerResult[];
49
+ }
50
+
51
+ export type EscrowEventHandler = (payload: unknown) => Promise<unknown> | unknown;
52
+
53
+ export interface EscrowStakeInfo {
54
+ agent_id?: string;
55
+ stake?: number;
56
+ }
57
+
58
+ export interface EscrowInfo {
59
+ proposal_id?: string;
60
+ from?: EscrowStakeInfo;
61
+ to?: EscrowStakeInfo;
62
+ }
63
+
64
+ export interface EscrowResult {
65
+ escrow?: {
66
+ proposal_id?: string;
67
+ };
68
+ }
69
+
70
+ export interface RatingChanges {
71
+ _escrow?: {
72
+ proposer_stake?: number;
73
+ acceptor_stake?: number;
74
+ settlement?: string;
75
+ settlement_reason?: string;
76
+ fault_party?: string;
77
+ transferred?: number;
78
+ burned?: number;
79
+ };
80
+ [key: string]: unknown;
81
+ }
82
+
83
+ export interface EscrowCreatedPayload {
84
+ event: typeof EscrowEvent.CREATED;
85
+ timestamp: number;
86
+ proposal_id: string;
87
+ from_agent: string;
88
+ to_agent: string;
89
+ proposer_stake: number;
90
+ acceptor_stake: number;
91
+ total_stake: number;
92
+ task: string;
93
+ amount?: number;
94
+ currency?: string;
95
+ expires?: number;
96
+ escrow_id: string;
97
+ }
98
+
99
+ export interface CompletionPayload {
100
+ event: typeof EscrowEvent.COMPLETION_SETTLED;
101
+ timestamp: number;
102
+ proposal_id: string;
103
+ from_agent: string;
104
+ to_agent: string;
105
+ completed_by?: string;
106
+ completion_proof?: string;
107
+ settlement: string;
108
+ stakes_returned: {
109
+ proposer: number;
110
+ acceptor: number;
111
+ };
112
+ rating_changes: {
113
+ [key: string]: unknown;
114
+ };
115
+ }
116
+
117
+ export interface DisputePayload {
118
+ event: typeof EscrowEvent.DISPUTE_SETTLED;
119
+ timestamp: number;
120
+ proposal_id: string;
121
+ from_agent: string;
122
+ to_agent: string;
123
+ disputed_by?: string;
124
+ dispute_reason?: string;
125
+ settlement: string;
126
+ settlement_reason?: string;
127
+ fault_determination?: string;
128
+ stakes_transferred?: number;
129
+ stakes_burned?: number;
130
+ rating_changes: {
131
+ [key: string]: unknown;
132
+ };
133
+ }
134
+
135
+ export interface EscrowReleasedPayload {
136
+ event: typeof EscrowEvent.RELEASED;
137
+ timestamp: number;
138
+ proposal_id: string;
139
+ from_agent?: string;
140
+ to_agent?: string;
141
+ stakes_released: {
142
+ proposer: number;
143
+ acceptor: number;
144
+ };
145
+ reason: string;
146
+ }
147
+
148
+ export interface ExtendedProposal extends Proposal {
149
+ proposer_stake?: number;
150
+ acceptor_stake?: number;
151
+ completed_by?: string;
152
+ completion_proof?: string;
153
+ disputed_by?: string;
154
+ dispute_reason?: string;
155
+ }
156
+
157
+ export class EscrowHooks {
158
+ private handlers: Map<EscrowEventType, Set<EscrowEventHandler>>;
159
+ private logger: Logger;
160
+ private continueOnError: boolean;
161
+
162
+ constructor(options: EscrowHooksOptions = {}) {
163
+ this.handlers = new Map();
164
+ this.logger = options.logger || console;
165
+ this.continueOnError = options.continueOnError !== false; // default true
166
+
167
+ // Initialize event handler sets
168
+ for (const event of Object.values(EscrowEvent)) {
169
+ this.handlers.set(event, new Set());
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Register a handler for an escrow event
175
+ * @param event - Event name from EscrowEvent
176
+ * @param handler - Async function(payload) to call
177
+ * @returns Unsubscribe function
178
+ */
179
+ on(event: EscrowEventType, handler: EscrowEventHandler): () => void {
180
+ if (!this.handlers.has(event)) {
181
+ throw new Error(`Unknown escrow event: ${event}`);
182
+ }
183
+
184
+ if (typeof handler !== 'function') {
185
+ throw new Error('Handler must be a function');
186
+ }
187
+
188
+ this.handlers.get(event)!.add(handler);
189
+
190
+ // Return unsubscribe function
191
+ return () => this.off(event, handler);
192
+ }
193
+
194
+ /**
195
+ * Remove a handler for an escrow event
196
+ * @param event - Event name
197
+ * @param handler - Handler to remove
198
+ */
199
+ off(event: EscrowEventType, handler: EscrowEventHandler): void {
200
+ if (this.handlers.has(event)) {
201
+ this.handlers.get(event)!.delete(handler);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Remove all handlers for an event (or all events)
207
+ * @param event - Optional event name
208
+ */
209
+ clear(event?: EscrowEventType): void {
210
+ if (event) {
211
+ if (this.handlers.has(event)) {
212
+ this.handlers.get(event)!.clear();
213
+ }
214
+ } else {
215
+ for (const handlers of this.handlers.values()) {
216
+ handlers.clear();
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Emit an escrow event to all registered handlers
223
+ * @param event - Event name
224
+ * @param payload - Event payload
225
+ * @returns Results from all handlers
226
+ */
227
+ async emit(event: EscrowEventType, payload: unknown): Promise<EmitResult> {
228
+ if (!this.handlers.has(event)) {
229
+ throw new Error(`Unknown escrow event: ${event}`);
230
+ }
231
+
232
+ const handlers = this.handlers.get(event)!;
233
+ if (handlers.size === 0) {
234
+ return { event, handled: false, results: [] };
235
+ }
236
+
237
+ const results: HandlerResult[] = [];
238
+ const errors: HandlerResult[] = [];
239
+
240
+ for (const handler of handlers) {
241
+ try {
242
+ const result = await handler(payload);
243
+ results.push({ success: true, result });
244
+ } catch (err) {
245
+ const error = err as Error;
246
+ const errorInfo: HandlerResult = {
247
+ success: false,
248
+ error: error.message,
249
+ stack: error.stack
250
+ };
251
+ errors.push(errorInfo);
252
+ results.push(errorInfo);
253
+
254
+ this.logger.error?.(`[EscrowHooks] Error in ${event} handler:`, error.message);
255
+
256
+ if (!this.continueOnError) {
257
+ break;
258
+ }
259
+ }
260
+ }
261
+
262
+ return {
263
+ event,
264
+ handled: true,
265
+ results,
266
+ errors: errors.length > 0 ? errors : undefined
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Check if any handlers are registered for an event
272
+ * @param event - Event name
273
+ * @returns True if handlers exist
274
+ */
275
+ hasHandlers(event: EscrowEventType): boolean {
276
+ return this.handlers.has(event) && this.handlers.get(event)!.size > 0;
277
+ }
278
+
279
+ /**
280
+ * Get count of handlers for an event
281
+ * @param event - Event name
282
+ * @returns Number of handlers
283
+ */
284
+ handlerCount(event: EscrowEventType): number {
285
+ return this.handlers.has(event) ? this.handlers.get(event)!.size : 0;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Create payload for escrow:created event
291
+ */
292
+ export function createEscrowCreatedPayload(
293
+ proposal: ExtendedProposal,
294
+ escrowResult: EscrowResult
295
+ ): EscrowCreatedPayload {
296
+ return {
297
+ event: EscrowEvent.CREATED,
298
+ timestamp: Date.now(),
299
+ proposal_id: proposal.id,
300
+ from_agent: proposal.from,
301
+ to_agent: proposal.to,
302
+ proposer_stake: proposal.proposer_stake || 0,
303
+ acceptor_stake: proposal.acceptor_stake || 0,
304
+ total_stake: (proposal.proposer_stake || 0) + (proposal.acceptor_stake || 0),
305
+ task: proposal.task,
306
+ amount: proposal.amount,
307
+ currency: proposal.currency,
308
+ expires: proposal.expires,
309
+ escrow_id: escrowResult.escrow?.proposal_id || proposal.id
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Create payload for settlement:completion event
315
+ */
316
+ export function createCompletionPayload(
317
+ proposal: ExtendedProposal,
318
+ ratingChanges?: RatingChanges
319
+ ): CompletionPayload {
320
+ const escrowInfo = ratingChanges?._escrow || {};
321
+ return {
322
+ event: EscrowEvent.COMPLETION_SETTLED,
323
+ timestamp: Date.now(),
324
+ proposal_id: proposal.id,
325
+ from_agent: proposal.from,
326
+ to_agent: proposal.to,
327
+ completed_by: proposal.completed_by,
328
+ completion_proof: proposal.completion_proof,
329
+ settlement: 'returned',
330
+ stakes_returned: {
331
+ proposer: escrowInfo.proposer_stake || 0,
332
+ acceptor: escrowInfo.acceptor_stake || 0
333
+ },
334
+ rating_changes: {
335
+ [proposal.from]: ratingChanges?.[proposal.from],
336
+ [proposal.to]: ratingChanges?.[proposal.to]
337
+ }
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Create payload for settlement:dispute event
343
+ */
344
+ export function createDisputePayload(
345
+ proposal: ExtendedProposal,
346
+ ratingChanges?: RatingChanges
347
+ ): DisputePayload {
348
+ const escrowInfo = ratingChanges?._escrow || {};
349
+ return {
350
+ event: EscrowEvent.DISPUTE_SETTLED,
351
+ timestamp: Date.now(),
352
+ proposal_id: proposal.id,
353
+ from_agent: proposal.from,
354
+ to_agent: proposal.to,
355
+ disputed_by: proposal.disputed_by,
356
+ dispute_reason: proposal.dispute_reason,
357
+ settlement: escrowInfo.settlement || 'settled',
358
+ settlement_reason: escrowInfo.settlement_reason,
359
+ fault_determination: escrowInfo.fault_party,
360
+ stakes_transferred: escrowInfo.transferred,
361
+ stakes_burned: escrowInfo.burned,
362
+ rating_changes: {
363
+ [proposal.from]: ratingChanges?.[proposal.from],
364
+ [proposal.to]: ratingChanges?.[proposal.to]
365
+ }
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Create payload for escrow:released event
371
+ */
372
+ export function createEscrowReleasedPayload(
373
+ proposalId: string,
374
+ escrow: EscrowInfo,
375
+ reason?: string
376
+ ): EscrowReleasedPayload {
377
+ return {
378
+ event: EscrowEvent.RELEASED,
379
+ timestamp: Date.now(),
380
+ proposal_id: proposalId,
381
+ from_agent: escrow.from?.agent_id,
382
+ to_agent: escrow.to?.agent_id,
383
+ stakes_released: {
384
+ proposer: escrow.from?.stake || 0,
385
+ acceptor: escrow.to?.stake || 0
386
+ },
387
+ reason: reason || 'expired'
388
+ };
389
+ }
390
+
391
+ export default EscrowHooks;