@tjamescouch/agentchat 0.22.1 → 0.23.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 (153) hide show
  1. package/Dockerfile +1 -1
  2. package/dist/bin/agentchat.d.ts +7 -0
  3. package/dist/bin/agentchat.d.ts.map +1 -0
  4. package/dist/bin/agentchat.js +1511 -0
  5. package/dist/bin/agentchat.js.map +1 -0
  6. package/dist/lib/allowlist.d.ts +77 -0
  7. package/dist/lib/allowlist.d.ts.map +1 -0
  8. package/dist/lib/allowlist.js +151 -0
  9. package/dist/lib/allowlist.js.map +1 -0
  10. package/dist/lib/client.d.ts +147 -0
  11. package/dist/lib/client.d.ts.map +1 -0
  12. package/dist/lib/client.js +704 -0
  13. package/dist/lib/client.js.map +1 -0
  14. package/dist/lib/daemon.d.ts +122 -0
  15. package/dist/lib/daemon.d.ts.map +1 -0
  16. package/dist/lib/daemon.js +523 -0
  17. package/dist/lib/daemon.js.map +1 -0
  18. package/dist/lib/deploy/akash.d.ts +271 -0
  19. package/dist/lib/deploy/akash.d.ts.map +1 -0
  20. package/dist/lib/deploy/akash.js +671 -0
  21. package/dist/lib/deploy/akash.js.map +1 -0
  22. package/dist/lib/deploy/config.d.ts +62 -0
  23. package/dist/lib/deploy/config.d.ts.map +1 -0
  24. package/dist/lib/deploy/config.js +116 -0
  25. package/dist/lib/deploy/config.js.map +1 -0
  26. package/dist/lib/deploy/docker.d.ts +37 -0
  27. package/dist/lib/deploy/docker.d.ts.map +1 -0
  28. package/dist/lib/deploy/docker.js +122 -0
  29. package/dist/lib/deploy/docker.js.map +1 -0
  30. package/dist/lib/deploy/index.d.ts +11 -0
  31. package/dist/lib/deploy/index.d.ts.map +1 -0
  32. package/dist/lib/deploy/index.js +11 -0
  33. package/dist/lib/deploy/index.js.map +1 -0
  34. package/dist/lib/escrow-hooks.d.ts +199 -0
  35. package/dist/lib/escrow-hooks.d.ts.map +1 -0
  36. package/dist/lib/escrow-hooks.js +221 -0
  37. package/dist/lib/escrow-hooks.js.map +1 -0
  38. package/dist/lib/identity.d.ts +134 -0
  39. package/dist/lib/identity.d.ts.map +1 -0
  40. package/dist/lib/identity.js +334 -0
  41. package/dist/lib/identity.js.map +1 -0
  42. package/dist/lib/jitter.d.ts +42 -0
  43. package/dist/lib/jitter.d.ts.map +1 -0
  44. package/{lib/jitter.ts → dist/lib/jitter.js} +10 -18
  45. package/dist/lib/jitter.js.map +1 -0
  46. package/dist/lib/proposals.d.ts +223 -0
  47. package/dist/lib/proposals.d.ts.map +1 -0
  48. package/dist/lib/proposals.js +379 -0
  49. package/dist/lib/proposals.js.map +1 -0
  50. package/dist/lib/protocol.d.ts +220 -0
  51. package/dist/lib/protocol.d.ts.map +1 -0
  52. package/dist/lib/protocol.js +507 -0
  53. package/dist/lib/protocol.js.map +1 -0
  54. package/dist/lib/receipts.d.ts +134 -0
  55. package/dist/lib/receipts.d.ts.map +1 -0
  56. package/dist/lib/receipts.js +270 -0
  57. package/dist/lib/receipts.js.map +1 -0
  58. package/dist/lib/reputation.d.ts +250 -0
  59. package/dist/lib/reputation.d.ts.map +1 -0
  60. package/dist/lib/reputation.js +586 -0
  61. package/dist/lib/reputation.js.map +1 -0
  62. package/dist/lib/security.d.ts +27 -0
  63. package/dist/lib/security.d.ts.map +1 -0
  64. package/dist/lib/security.js +150 -0
  65. package/dist/lib/security.js.map +1 -0
  66. package/dist/lib/server/handlers/admin.d.ts +26 -0
  67. package/dist/lib/server/handlers/admin.d.ts.map +1 -0
  68. package/dist/lib/server/handlers/admin.js +76 -0
  69. package/dist/lib/server/handlers/admin.js.map +1 -0
  70. package/dist/lib/server/handlers/identity.d.ts +36 -0
  71. package/dist/lib/server/handlers/identity.d.ts.map +1 -0
  72. package/dist/lib/server/handlers/identity.js +330 -0
  73. package/dist/lib/server/handlers/identity.js.map +1 -0
  74. package/dist/lib/server/handlers/index.d.ts +10 -0
  75. package/dist/lib/server/handlers/index.d.ts.map +1 -0
  76. package/dist/lib/server/handlers/index.js +15 -0
  77. package/dist/lib/server/handlers/index.js.map +1 -0
  78. package/dist/lib/server/handlers/message.d.ts +47 -0
  79. package/dist/lib/server/handlers/message.d.ts.map +1 -0
  80. package/dist/lib/server/handlers/message.js +265 -0
  81. package/dist/lib/server/handlers/message.js.map +1 -0
  82. package/dist/lib/server/handlers/presence.d.ts +18 -0
  83. package/dist/lib/server/handlers/presence.d.ts.map +1 -0
  84. package/dist/lib/server/handlers/presence.js +35 -0
  85. package/dist/lib/server/handlers/presence.js.map +1 -0
  86. package/dist/lib/server/handlers/proposal.d.ts +38 -0
  87. package/dist/lib/server/handlers/proposal.d.ts.map +1 -0
  88. package/dist/lib/server/handlers/proposal.js +273 -0
  89. package/dist/lib/server/handlers/proposal.js.map +1 -0
  90. package/dist/lib/server/handlers/skills.d.ts +22 -0
  91. package/dist/lib/server/handlers/skills.d.ts.map +1 -0
  92. package/dist/lib/server/handlers/skills.js +119 -0
  93. package/dist/lib/server/handlers/skills.js.map +1 -0
  94. package/dist/lib/server-directory.d.ts +85 -0
  95. package/dist/lib/server-directory.d.ts.map +1 -0
  96. package/dist/lib/server-directory.js +177 -0
  97. package/dist/lib/server-directory.js.map +1 -0
  98. package/dist/lib/server.d.ts +162 -0
  99. package/dist/lib/server.d.ts.map +1 -0
  100. package/dist/lib/server.js +602 -0
  101. package/dist/lib/server.js.map +1 -0
  102. package/dist/lib/types.d.ts +461 -0
  103. package/dist/lib/types.d.ts.map +1 -0
  104. package/dist/lib/types.js +98 -0
  105. package/dist/lib/types.js.map +1 -0
  106. package/package.json +22 -13
  107. package/bin/agentchat.js +0 -1617
  108. package/bin/agentchat.ts +0 -1812
  109. package/lib/allowlist.js +0 -162
  110. package/lib/chat.py +0 -241
  111. package/lib/client.js +0 -821
  112. package/lib/client.ts +0 -877
  113. package/lib/daemon.js +0 -562
  114. package/lib/daemon.ts +0 -662
  115. package/lib/deploy/akash.js +0 -811
  116. package/lib/deploy/config.js +0 -128
  117. package/lib/deploy/docker.js +0 -132
  118. package/lib/deploy/index.js +0 -24
  119. package/lib/elo_swarm.py +0 -569
  120. package/lib/escrow-hooks.js +0 -237
  121. package/lib/escrow-hooks.ts +0 -391
  122. package/lib/identity.js +0 -376
  123. package/lib/identity.ts +0 -412
  124. package/lib/jitter.js +0 -54
  125. package/lib/proposals.js +0 -426
  126. package/lib/proposals.ts +0 -612
  127. package/lib/protocol.js +0 -516
  128. package/lib/receipts.js +0 -294
  129. package/lib/receipts.ts +0 -359
  130. package/lib/reputation.js +0 -664
  131. package/lib/reputation.ts +0 -790
  132. package/lib/security.js +0 -183
  133. package/lib/server/handlers/admin.js +0 -94
  134. package/lib/server/handlers/identity.js +0 -258
  135. package/lib/server/handlers/index.js +0 -42
  136. package/lib/server/handlers/message.js +0 -319
  137. package/lib/server/handlers/presence.js +0 -45
  138. package/lib/server/handlers/proposal.js +0 -358
  139. package/lib/server/handlers/skills.js +0 -141
  140. package/lib/server-directory.js +0 -190
  141. package/lib/server-directory.ts +0 -232
  142. package/lib/server.js +0 -633
  143. package/lib/server.ts +0 -698
  144. package/lib/supervisor/USAGE.md +0 -110
  145. package/lib/supervisor/agent-health.sh +0 -107
  146. package/lib/supervisor/agent-monitor.sh +0 -123
  147. package/lib/supervisor/agent-supervisor.sh +0 -135
  148. package/lib/supervisor/agentctl.sh +0 -266
  149. package/lib/supervisor/god-backup.sh +0 -126
  150. package/lib/supervisor/god-watchdog.sh +0 -107
  151. package/lib/supervisor/killswitch.sh +0 -43
  152. package/lib/supervisor/notify.sh +0 -19
  153. package/lib/types.ts +0 -433
package/lib/reputation.ts DELETED
@@ -1,790 +0,0 @@
1
- /**
2
- * AgentChat Reputation Module
3
- * ELO-based rating system for agent reputation
4
- *
5
- * Adapts chess ELO for cooperative agent coordination:
6
- * - Each agent starts at 1200
7
- * - On COMPLETE: both agents gain points, scaled by counterparty rating
8
- * - On DISPUTE: at-fault party loses points
9
- * - K-factor varies by experience (new agents move faster)
10
- */
11
-
12
- import fs from 'fs';
13
- import fsp from 'fs/promises';
14
- import path from 'path';
15
- import os from 'os';
16
-
17
- // ============ Types ============
18
-
19
- export interface RatingRecord {
20
- rating: number;
21
- transactions: number;
22
- updated: string | null;
23
- }
24
-
25
- export interface AgentRating {
26
- agentId: string;
27
- rating: number;
28
- transactions: number;
29
- updated: string | null;
30
- isNew: boolean;
31
- }
32
-
33
- export interface StakeCheck {
34
- canStake: boolean;
35
- available: number;
36
- reason?: string;
37
- }
38
-
39
- export interface EscrowParty {
40
- agent_id: string;
41
- stake: number;
42
- }
43
-
44
- export interface Escrow {
45
- proposal_id: string;
46
- created_at: number;
47
- from: EscrowParty;
48
- to: EscrowParty;
49
- status: 'active' | 'released' | 'settled';
50
- expires_at: number | null;
51
- settled_at: number | null;
52
- settlement_reason: string | null;
53
- }
54
-
55
- export interface EscrowResult {
56
- success?: boolean;
57
- released?: boolean;
58
- error?: string;
59
- escrow?: Escrow;
60
- }
61
-
62
- export interface Receipt {
63
- type?: string;
64
- status?: string;
65
- proposal_id?: string;
66
- proposal?: {
67
- from?: string;
68
- to?: string;
69
- amount?: number;
70
- };
71
- from?: string;
72
- to?: string;
73
- amount?: number;
74
- disputed_by?: string;
75
- completed_at?: number;
76
- disputed_at?: number;
77
- stored_at?: number;
78
- }
79
-
80
- export interface RatingChange {
81
- oldRating: number;
82
- newRating: number;
83
- change: number;
84
- transactions: number;
85
- }
86
-
87
- export interface EscrowSettlement {
88
- proposer_stake: number;
89
- acceptor_stake: number;
90
- settlement: 'returned' | 'transferred' | 'burned';
91
- transferred_to?: string;
92
- transferred_amount?: number;
93
- burned_amount?: number;
94
- }
95
-
96
- export interface RatingChanges {
97
- [agentId: string]: RatingChange;
98
- _escrow?: EscrowSettlement;
99
- }
100
-
101
- export interface LeaderboardEntry {
102
- agentId: string;
103
- rating: number;
104
- transactions: number;
105
- updated: string | null;
106
- }
107
-
108
- export interface ReputationStats {
109
- totalAgents: number;
110
- averageRating: number;
111
- highestRating: number;
112
- lowestRating: number;
113
- totalTransactions: number;
114
- }
115
-
116
- // ============ Constants ============
117
-
118
- // Default ratings file location
119
- const AGENTCHAT_DIR = path.join(process.cwd(), '.agentchat');
120
- export const DEFAULT_RATINGS_PATH = path.join(AGENTCHAT_DIR, 'ratings.json');
121
-
122
- // ELO constants
123
- export const DEFAULT_RATING = 1200;
124
- export const ELO_DIVISOR = 400; // Standard ELO divisor
125
- export const MINIMUM_RATING = 100; // Floor - can't drop below this
126
-
127
- // K-factor thresholds
128
- const K_FACTOR_NEW = 32; // < 30 transactions
129
- const K_FACTOR_INTERMEDIATE = 24; // < 100 transactions
130
- const K_FACTOR_ESTABLISHED = 16; // >= 100 transactions
131
-
132
- const TRANSACTIONS_NEW = 30;
133
- const TRANSACTIONS_INTERMEDIATE = 100;
134
-
135
- // ============ Helper Functions ============
136
-
137
- /**
138
- * Calculate expected outcome (standard ELO formula)
139
- * E = 1 / (1 + 10^((R_opponent - R_self) / 400))
140
- *
141
- * @param selfRating - Your rating
142
- * @param opponentRating - Counterparty rating
143
- * @returns Expected outcome (0-1)
144
- */
145
- export function calculateExpected(selfRating: number, opponentRating: number): number {
146
- const exponent = (opponentRating - selfRating) / ELO_DIVISOR;
147
- return 1 / (1 + Math.pow(10, exponent));
148
- }
149
-
150
- /**
151
- * Get K-factor based on transaction count
152
- * New agents have higher K (volatile), established agents lower K (stable)
153
- *
154
- * @param transactions - Number of completed transactions
155
- * @returns K-factor
156
- */
157
- export function getKFactor(transactions: number): number {
158
- if (transactions < TRANSACTIONS_NEW) {
159
- return K_FACTOR_NEW;
160
- } else if (transactions < TRANSACTIONS_INTERMEDIATE) {
161
- return K_FACTOR_INTERMEDIATE;
162
- }
163
- return K_FACTOR_ESTABLISHED;
164
- }
165
-
166
- /**
167
- * Calculate effective K-factor with optional task value weighting
168
- * effective_K = K * (1 + log10(amount + 1))
169
- *
170
- * @param baseK - Base K-factor
171
- * @param amount - Task value/amount (optional)
172
- * @returns Effective K-factor
173
- */
174
- export function getEffectiveK(baseK: number, amount: number = 0): number {
175
- if (!amount || amount <= 0) {
176
- return baseK;
177
- }
178
- // Weight by task value: higher value = more rating movement
179
- // Cap the multiplier to prevent extreme swings
180
- const multiplier = Math.min(1 + Math.log10(amount + 1), 3);
181
- return baseK * multiplier;
182
- }
183
-
184
- /**
185
- * Calculate rating change for a completion (cooperative outcome)
186
- * Both parties gain, but you gain more when completing with higher-rated counterparty
187
- *
188
- * @param selfRating - Your current rating
189
- * @param counterpartyRating - Counterparty's rating
190
- * @param kFactor - Your K-factor
191
- * @param amount - Optional task value
192
- * @returns Rating change (positive)
193
- */
194
- export function calculateCompletionGain(
195
- selfRating: number,
196
- counterpartyRating: number,
197
- kFactor: number,
198
- amount: number = 0
199
- ): number {
200
- const expected = calculateExpected(selfRating, counterpartyRating);
201
- const effectiveK = getEffectiveK(kFactor, amount);
202
-
203
- // Gain = K * (1 - E)
204
- // You gain more when completing with higher-rated counterparty (lower E)
205
- const gain = effectiveK * (1 - expected);
206
-
207
- // Minimum gain of 1 point for any completion
208
- return Math.max(1, Math.round(gain));
209
- }
210
-
211
- /**
212
- * Calculate rating change for a dispute (loss for at-fault party)
213
- *
214
- * @param selfRating - Your current rating
215
- * @param counterpartyRating - Counterparty's rating
216
- * @param kFactor - Your K-factor
217
- * @param amount - Optional task value
218
- * @returns Rating change (negative)
219
- */
220
- export function calculateDisputeLoss(
221
- selfRating: number,
222
- counterpartyRating: number,
223
- kFactor: number,
224
- amount: number = 0
225
- ): number {
226
- const expected = calculateExpected(selfRating, counterpartyRating);
227
- const effectiveK = getEffectiveK(kFactor, amount);
228
-
229
- // Loss = K * E
230
- // You lose more when you were expected to succeed (higher E)
231
- const loss = effectiveK * expected;
232
-
233
- // Minimum loss of 1 point
234
- return -Math.max(1, Math.round(loss));
235
- }
236
-
237
- // ============ ReputationStore Class ============
238
-
239
- /**
240
- * Reputation Store - manages agent ratings
241
- */
242
- export class ReputationStore {
243
- private ratingsPath: string;
244
- private _ratings: Record<string, RatingRecord> | null;
245
- private _escrows: Map<string, Escrow>;
246
-
247
- constructor(ratingsPath: string = DEFAULT_RATINGS_PATH) {
248
- this.ratingsPath = ratingsPath;
249
- this._ratings = null; // Lazy load
250
- this._escrows = new Map(); // proposalId -> escrow record
251
- }
252
-
253
- /**
254
- * Load ratings from file
255
- */
256
- async load(): Promise<Record<string, RatingRecord>> {
257
- try {
258
- const content = await fsp.readFile(this.ratingsPath, 'utf-8');
259
- this._ratings = JSON.parse(content);
260
- } catch (err: any) {
261
- if (err.code === 'ENOENT') {
262
- this._ratings = {}; // No ratings file yet
263
- } else {
264
- throw err;
265
- }
266
- }
267
- return this._ratings!;
268
- }
269
-
270
- /**
271
- * Save ratings to file
272
- */
273
- async save(): Promise<void> {
274
- await fsp.mkdir(path.dirname(this.ratingsPath), { recursive: true });
275
- await fsp.writeFile(
276
- this.ratingsPath,
277
- JSON.stringify(this._ratings, null, 2),
278
- { mode: 0o600 }
279
- );
280
- }
281
-
282
- /**
283
- * Ensure ratings are loaded
284
- */
285
- private async _ensureLoaded(): Promise<void> {
286
- if (this._ratings === null) {
287
- await this.load();
288
- }
289
- }
290
-
291
- /**
292
- * Normalize agent ID (ensure @ prefix)
293
- */
294
- private _normalizeId(agentId: string): string {
295
- return agentId.startsWith('@') ? agentId : `@${agentId}`;
296
- }
297
-
298
- /**
299
- * Get rating for an agent
300
- * Returns default rating if agent not found
301
- */
302
- async getRating(agentId: string): Promise<AgentRating> {
303
- await this._ensureLoaded();
304
- const id = this._normalizeId(agentId);
305
- const record = this._ratings![id];
306
-
307
- if (!record) {
308
- return {
309
- agentId: id,
310
- rating: DEFAULT_RATING,
311
- transactions: 0,
312
- updated: null,
313
- isNew: true
314
- };
315
- }
316
-
317
- return {
318
- agentId: id,
319
- rating: record.rating,
320
- transactions: record.transactions,
321
- updated: record.updated,
322
- isNew: false
323
- };
324
- }
325
-
326
- /**
327
- * Get K-factor for an agent
328
- */
329
- async getAgentKFactor(agentId: string): Promise<number> {
330
- const record = await this.getRating(agentId);
331
- return getKFactor(record.transactions);
332
- }
333
-
334
- /**
335
- * Get total escrowed ELO for an agent
336
- */
337
- getEscrowedAmount(agentId: string): number {
338
- const id = this._normalizeId(agentId);
339
- let total = 0;
340
- for (const escrow of this._escrows.values()) {
341
- if (escrow.status === 'active') {
342
- if (escrow.from.agent_id === id) {
343
- total += escrow.from.stake;
344
- }
345
- if (escrow.to.agent_id === id) {
346
- total += escrow.to.stake;
347
- }
348
- }
349
- }
350
- return total;
351
- }
352
-
353
- /**
354
- * Get available rating for staking (rating - escrowed - minimum floor)
355
- */
356
- async getAvailableRating(agentId: string): Promise<number> {
357
- const record = await this.getRating(agentId);
358
- const escrowed = this.getEscrowedAmount(agentId);
359
- const available = record.rating - escrowed - MINIMUM_RATING;
360
- return Math.max(0, available);
361
- }
362
-
363
- /**
364
- * Check if agent can stake the requested amount
365
- */
366
- async canStake(agentId: string, amount: number): Promise<StakeCheck> {
367
- if (!amount || amount <= 0) {
368
- return { canStake: true, available: await this.getAvailableRating(agentId) };
369
- }
370
-
371
- const available = await this.getAvailableRating(agentId);
372
-
373
- if (amount > available) {
374
- return {
375
- canStake: false,
376
- available,
377
- reason: `Insufficient ELO. Available: ${available}, Requested: ${amount}`
378
- };
379
- }
380
-
381
- return { canStake: true, available };
382
- }
383
-
384
- /**
385
- * Create escrow for a proposal
386
- * Called when proposal is accepted with stakes
387
- */
388
- async createEscrow(
389
- proposalId: string,
390
- fromStake: EscrowParty,
391
- toStake: EscrowParty,
392
- expiresAt: number | null = null
393
- ): Promise<EscrowResult> {
394
- // Validate both parties can stake
395
- if (fromStake.stake > 0) {
396
- const canFrom = await this.canStake(fromStake.agent_id, fromStake.stake);
397
- if (!canFrom.canStake) {
398
- return { success: false, error: `Proposer: ${canFrom.reason}` };
399
- }
400
- }
401
-
402
- if (toStake.stake > 0) {
403
- const canTo = await this.canStake(toStake.agent_id, toStake.stake);
404
- if (!canTo.canStake) {
405
- return { success: false, error: `Acceptor: ${canTo.reason}` };
406
- }
407
- }
408
-
409
- const escrow: Escrow = {
410
- proposal_id: proposalId,
411
- created_at: Date.now(),
412
- from: {
413
- agent_id: this._normalizeId(fromStake.agent_id),
414
- stake: fromStake.stake || 0
415
- },
416
- to: {
417
- agent_id: this._normalizeId(toStake.agent_id),
418
- stake: toStake.stake || 0
419
- },
420
- status: 'active',
421
- expires_at: expiresAt,
422
- settled_at: null,
423
- settlement_reason: null
424
- };
425
-
426
- this._escrows.set(proposalId, escrow);
427
- return { success: true, escrow };
428
- }
429
-
430
- /**
431
- * Get escrow for a proposal
432
- */
433
- getEscrow(proposalId: string): Escrow | null {
434
- return this._escrows.get(proposalId) || null;
435
- }
436
-
437
- /**
438
- * Release escrow (return stakes to both parties, no rating change)
439
- * Used for proposal expiration
440
- */
441
- releaseEscrow(proposalId: string): EscrowResult {
442
- const escrow = this._escrows.get(proposalId);
443
- if (!escrow) {
444
- return { released: false, error: 'Escrow not found' };
445
- }
446
-
447
- if (escrow.status !== 'active') {
448
- return { released: false, error: `Escrow already ${escrow.status}` };
449
- }
450
-
451
- escrow.status = 'released';
452
- escrow.settled_at = Date.now();
453
- escrow.settlement_reason = 'expired';
454
-
455
- return { released: true, escrow };
456
- }
457
-
458
- /**
459
- * Update rating for an agent
460
- */
461
- private async _updateAgent(agentId: string, ratingChange: number): Promise<RatingRecord> {
462
- await this._ensureLoaded();
463
- const id = this._normalizeId(agentId);
464
-
465
- if (!this._ratings![id]) {
466
- this._ratings![id] = {
467
- rating: DEFAULT_RATING,
468
- transactions: 0,
469
- updated: null
470
- };
471
- }
472
-
473
- this._ratings![id].rating = Math.max(100, this._ratings![id].rating + ratingChange);
474
- this._ratings![id].transactions += 1;
475
- this._ratings![id].updated = new Date().toISOString();
476
-
477
- return this._ratings![id];
478
- }
479
-
480
- /**
481
- * Process a COMPLETE receipt - both parties gain (halved gains with staking)
482
- *
483
- * @param receipt - The COMPLETE receipt
484
- * @returns Rating changes for both parties
485
- */
486
- async processCompletion(receipt: Receipt): Promise<RatingChanges> {
487
- // Extract parties from receipt
488
- const party1 = receipt.proposal?.from || receipt.from;
489
- const party2 = receipt.proposal?.to || receipt.to;
490
- const amount = receipt.proposal?.amount || receipt.amount || 0;
491
- const proposalId = receipt.proposal_id;
492
-
493
- if (!party1 || !party2) {
494
- throw new Error('Receipt missing party information');
495
- }
496
-
497
- // Get current ratings
498
- const rating1 = await this.getRating(party1);
499
- const rating2 = await this.getRating(party2);
500
-
501
- // Calculate gains (halved for staking model)
502
- const k1 = getKFactor(rating1.transactions);
503
- const k2 = getKFactor(rating2.transactions);
504
-
505
- const fullGain1 = calculateCompletionGain(rating1.rating, rating2.rating, k1, amount);
506
- const fullGain2 = calculateCompletionGain(rating2.rating, rating1.rating, k2, amount);
507
-
508
- // Half the gains (staking model: split gains to balance inflation)
509
- const gain1 = Math.max(1, Math.round(fullGain1 / 2));
510
- const gain2 = Math.max(1, Math.round(fullGain2 / 2));
511
-
512
- // Settle escrow if exists (return stakes)
513
- let escrowSettlement: EscrowSettlement | null = null;
514
- if (proposalId) {
515
- const escrow = this._escrows.get(proposalId);
516
- if (escrow && escrow.status === 'active') {
517
- escrow.status = 'settled';
518
- escrow.settled_at = Date.now();
519
- escrow.settlement_reason = 'completed';
520
- escrowSettlement = {
521
- proposer_stake: escrow.from.stake,
522
- acceptor_stake: escrow.to.stake,
523
- settlement: 'returned'
524
- };
525
- }
526
- }
527
-
528
- // Apply updates
529
- const updated1 = await this._updateAgent(party1, gain1);
530
- const updated2 = await this._updateAgent(party2, gain2);
531
-
532
- // Save
533
- await this.save();
534
-
535
- const result: RatingChanges = {
536
- [party1]: {
537
- oldRating: rating1.rating,
538
- newRating: updated1.rating,
539
- change: gain1,
540
- transactions: updated1.transactions
541
- },
542
- [party2]: {
543
- oldRating: rating2.rating,
544
- newRating: updated2.rating,
545
- change: gain2,
546
- transactions: updated2.transactions
547
- }
548
- };
549
-
550
- if (escrowSettlement) {
551
- result._escrow = escrowSettlement;
552
- }
553
-
554
- return result;
555
- }
556
-
557
- /**
558
- * Process a DISPUTE receipt
559
- * If disputed_by is set, they are the "winner" (counterparty is at fault)
560
- * Otherwise, both parties lose (mutual fault)
561
- * Stakes are transferred to winner or burned on mutual fault
562
- *
563
- * @param receipt - The DISPUTE receipt
564
- * @returns Rating changes for both parties
565
- */
566
- async processDispute(receipt: Receipt): Promise<RatingChanges> {
567
- const party1 = receipt.proposal?.from || receipt.from;
568
- const party2 = receipt.proposal?.to || receipt.to;
569
- const disputedBy = receipt.disputed_by;
570
- const amount = receipt.proposal?.amount || receipt.amount || 0;
571
- const proposalId = receipt.proposal_id;
572
-
573
- if (!party1 || !party2) {
574
- throw new Error('Receipt missing party information');
575
- }
576
-
577
- const rating1 = await this.getRating(party1);
578
- const rating2 = await this.getRating(party2);
579
-
580
- const k1 = getKFactor(rating1.transactions);
581
- const k2 = getKFactor(rating2.transactions);
582
-
583
- // Get escrow info for stake calculations
584
- let escrow: Escrow | null = null;
585
- let stake1 = 0, stake2 = 0;
586
- if (proposalId) {
587
- escrow = this._escrows.get(proposalId) || null;
588
- if (escrow && escrow.status === 'active') {
589
- stake1 = escrow.from.agent_id === this._normalizeId(party1)
590
- ? escrow.from.stake
591
- : escrow.to.stake;
592
- stake2 = escrow.from.agent_id === this._normalizeId(party2)
593
- ? escrow.from.stake
594
- : escrow.to.stake;
595
- }
596
- }
597
-
598
- let change1: number, change2: number;
599
- let escrowSettlement: EscrowSettlement | null = null;
600
-
601
- if (disputedBy) {
602
- // The disputer is the "winner", counterparty at fault
603
- const atFault = disputedBy === party1 ? party2 : party1;
604
-
605
- if (atFault === party1) {
606
- // Party1 at fault: loses ELO + loses stake to party2
607
- change1 = calculateDisputeLoss(rating1.rating, rating2.rating, k1, amount) - stake1;
608
- change2 = Math.round(Math.abs(calculateDisputeLoss(rating1.rating, rating2.rating, k1, amount)) * 0.5) + stake1;
609
- escrowSettlement = {
610
- proposer_stake: escrow?.from.stake || 0,
611
- acceptor_stake: escrow?.to.stake || 0,
612
- settlement: 'transferred',
613
- transferred_to: party2,
614
- transferred_amount: stake1
615
- };
616
- } else {
617
- // Party2 at fault: loses ELO + loses stake to party1
618
- change2 = calculateDisputeLoss(rating2.rating, rating1.rating, k2, amount) - stake2;
619
- change1 = Math.round(Math.abs(calculateDisputeLoss(rating2.rating, rating1.rating, k2, amount)) * 0.5) + stake2;
620
- escrowSettlement = {
621
- proposer_stake: escrow?.from.stake || 0,
622
- acceptor_stake: escrow?.to.stake || 0,
623
- settlement: 'transferred',
624
- transferred_to: party1,
625
- transferred_amount: stake2
626
- };
627
- }
628
- } else {
629
- // Mutual fault - both lose ELO + both stakes burned
630
- change1 = calculateDisputeLoss(rating1.rating, rating2.rating, k1, amount) - stake1;
631
- change2 = calculateDisputeLoss(rating2.rating, rating1.rating, k2, amount) - stake2;
632
- escrowSettlement = {
633
- proposer_stake: escrow?.from.stake || 0,
634
- acceptor_stake: escrow?.to.stake || 0,
635
- settlement: 'burned',
636
- burned_amount: stake1 + stake2
637
- };
638
- }
639
-
640
- // Settle escrow
641
- if (escrow && escrow.status === 'active') {
642
- escrow.status = 'settled';
643
- escrow.settled_at = Date.now();
644
- escrow.settlement_reason = 'disputed';
645
- }
646
-
647
- const updated1 = await this._updateAgent(party1, change1);
648
- const updated2 = await this._updateAgent(party2, change2);
649
-
650
- await this.save();
651
-
652
- const result: RatingChanges = {
653
- [party1]: {
654
- oldRating: rating1.rating,
655
- newRating: updated1.rating,
656
- change: change1,
657
- transactions: updated1.transactions
658
- },
659
- [party2]: {
660
- oldRating: rating2.rating,
661
- newRating: updated2.rating,
662
- change: change2,
663
- transactions: updated2.transactions
664
- }
665
- };
666
-
667
- if (escrowSettlement) {
668
- result._escrow = escrowSettlement;
669
- }
670
-
671
- return result;
672
- }
673
-
674
- /**
675
- * Process a receipt (routes to completion or dispute)
676
- */
677
- async updateRatings(receipt: Receipt): Promise<RatingChanges | null> {
678
- const type = receipt.type || receipt.status;
679
-
680
- if (type === 'COMPLETE' || type === 'completed') {
681
- return this.processCompletion(receipt);
682
- } else if (type === 'DISPUTE' || type === 'disputed') {
683
- return this.processDispute(receipt);
684
- }
685
-
686
- // Not a rating-relevant receipt type
687
- return null;
688
- }
689
-
690
- /**
691
- * Export all ratings
692
- */
693
- async exportRatings(): Promise<Record<string, RatingRecord>> {
694
- await this._ensureLoaded();
695
- return { ...this._ratings! };
696
- }
697
-
698
- /**
699
- * Get all ratings sorted by rating (descending)
700
- */
701
- async getLeaderboard(limit: number = 50): Promise<LeaderboardEntry[]> {
702
- await this._ensureLoaded();
703
-
704
- const entries = Object.entries(this._ratings!)
705
- .map(([id, data]) => ({
706
- agentId: id,
707
- rating: data.rating,
708
- transactions: data.transactions,
709
- updated: data.updated
710
- }))
711
- .sort((a, b) => b.rating - a.rating)
712
- .slice(0, limit);
713
-
714
- return entries;
715
- }
716
-
717
- /**
718
- * Recalculate all ratings from receipt history
719
- *
720
- * @param receipts - Array of receipts to process
721
- */
722
- async recalculateFromReceipts(receipts: Receipt[]): Promise<Record<string, RatingRecord>> {
723
- // Reset ratings
724
- this._ratings = {};
725
-
726
- // Sort receipts by timestamp
727
- const sorted = [...receipts].sort((a, b) => {
728
- const tsA = a.completed_at || a.disputed_at || a.stored_at || 0;
729
- const tsB = b.completed_at || b.disputed_at || b.stored_at || 0;
730
- return tsA - tsB;
731
- });
732
-
733
- // Process each receipt
734
- for (const receipt of sorted) {
735
- try {
736
- await this.updateRatings(receipt);
737
- } catch (err: any) {
738
- // Skip invalid receipts
739
- console.error(`Skipping invalid receipt: ${err.message}`);
740
- }
741
- }
742
-
743
- // Save is called by updateRatings, but save final state
744
- await this.save();
745
-
746
- return this._ratings;
747
- }
748
-
749
- /**
750
- * Get statistics about the rating system
751
- */
752
- async getStats(): Promise<ReputationStats> {
753
- await this._ensureLoaded();
754
-
755
- const ratings = Object.values(this._ratings!).map(r => r.rating);
756
-
757
- if (ratings.length === 0) {
758
- return {
759
- totalAgents: 0,
760
- averageRating: DEFAULT_RATING,
761
- highestRating: DEFAULT_RATING,
762
- lowestRating: DEFAULT_RATING,
763
- totalTransactions: 0
764
- };
765
- }
766
-
767
- const totalTransactions = Object.values(this._ratings!)
768
- .reduce((sum, r) => sum + r.transactions, 0);
769
-
770
- return {
771
- totalAgents: ratings.length,
772
- averageRating: Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length),
773
- highestRating: Math.max(...ratings),
774
- lowestRating: Math.min(...ratings),
775
- totalTransactions
776
- };
777
- }
778
- }
779
-
780
- // ============ Default Instance ============
781
-
782
- // Default instance for convenience
783
- let defaultStore: ReputationStore | null = null;
784
-
785
- export function getDefaultStore(): ReputationStore {
786
- if (!defaultStore) {
787
- defaultStore = new ReputationStore();
788
- }
789
- return defaultStore;
790
- }