@tjamescouch/agentchat 0.22.0 → 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.
- package/Dockerfile +1 -1
- package/dist/bin/agentchat.d.ts +7 -0
- package/dist/bin/agentchat.d.ts.map +1 -0
- package/dist/bin/agentchat.js +1511 -0
- package/dist/bin/agentchat.js.map +1 -0
- package/dist/lib/allowlist.d.ts +77 -0
- package/dist/lib/allowlist.d.ts.map +1 -0
- package/dist/lib/allowlist.js +151 -0
- package/dist/lib/allowlist.js.map +1 -0
- package/dist/lib/client.d.ts +147 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +704 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/daemon.d.ts +122 -0
- package/dist/lib/daemon.d.ts.map +1 -0
- package/dist/lib/daemon.js +523 -0
- package/dist/lib/daemon.js.map +1 -0
- package/dist/lib/deploy/akash.d.ts +271 -0
- package/dist/lib/deploy/akash.d.ts.map +1 -0
- package/dist/lib/deploy/akash.js +671 -0
- package/dist/lib/deploy/akash.js.map +1 -0
- package/dist/lib/deploy/config.d.ts +62 -0
- package/dist/lib/deploy/config.d.ts.map +1 -0
- package/dist/lib/deploy/config.js +116 -0
- package/dist/lib/deploy/config.js.map +1 -0
- package/dist/lib/deploy/docker.d.ts +37 -0
- package/dist/lib/deploy/docker.d.ts.map +1 -0
- package/dist/lib/deploy/docker.js +122 -0
- package/dist/lib/deploy/docker.js.map +1 -0
- package/dist/lib/deploy/index.d.ts +11 -0
- package/dist/lib/deploy/index.d.ts.map +1 -0
- package/dist/lib/deploy/index.js +11 -0
- package/dist/lib/deploy/index.js.map +1 -0
- package/dist/lib/escrow-hooks.d.ts +199 -0
- package/dist/lib/escrow-hooks.d.ts.map +1 -0
- package/dist/lib/escrow-hooks.js +221 -0
- package/dist/lib/escrow-hooks.js.map +1 -0
- package/dist/lib/identity.d.ts +134 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +334 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/jitter.d.ts +42 -0
- package/dist/lib/jitter.d.ts.map +1 -0
- package/{lib → dist/lib}/jitter.js +16 -19
- package/dist/lib/jitter.js.map +1 -0
- package/dist/lib/proposals.d.ts +223 -0
- package/dist/lib/proposals.d.ts.map +1 -0
- package/dist/lib/proposals.js +379 -0
- package/dist/lib/proposals.js.map +1 -0
- package/dist/lib/protocol.d.ts +220 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +507 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/receipts.d.ts +134 -0
- package/dist/lib/receipts.d.ts.map +1 -0
- package/dist/lib/receipts.js +270 -0
- package/dist/lib/receipts.js.map +1 -0
- package/dist/lib/reputation.d.ts +250 -0
- package/dist/lib/reputation.d.ts.map +1 -0
- package/dist/lib/reputation.js +586 -0
- package/dist/lib/reputation.js.map +1 -0
- package/dist/lib/security.d.ts +27 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +150 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/server/handlers/admin.d.ts +26 -0
- package/dist/lib/server/handlers/admin.d.ts.map +1 -0
- package/dist/lib/server/handlers/admin.js +76 -0
- package/dist/lib/server/handlers/admin.js.map +1 -0
- package/dist/lib/server/handlers/identity.d.ts +36 -0
- package/dist/lib/server/handlers/identity.d.ts.map +1 -0
- package/dist/lib/server/handlers/identity.js +330 -0
- package/dist/lib/server/handlers/identity.js.map +1 -0
- package/dist/lib/server/handlers/index.d.ts +10 -0
- package/dist/lib/server/handlers/index.d.ts.map +1 -0
- package/dist/lib/server/handlers/index.js +15 -0
- package/dist/lib/server/handlers/index.js.map +1 -0
- package/dist/lib/server/handlers/message.d.ts +47 -0
- package/dist/lib/server/handlers/message.d.ts.map +1 -0
- package/dist/lib/server/handlers/message.js +265 -0
- package/dist/lib/server/handlers/message.js.map +1 -0
- package/dist/lib/server/handlers/presence.d.ts +18 -0
- package/dist/lib/server/handlers/presence.d.ts.map +1 -0
- package/dist/lib/server/handlers/presence.js +35 -0
- package/dist/lib/server/handlers/presence.js.map +1 -0
- package/dist/lib/server/handlers/proposal.d.ts +38 -0
- package/dist/lib/server/handlers/proposal.d.ts.map +1 -0
- package/dist/lib/server/handlers/proposal.js +273 -0
- package/dist/lib/server/handlers/proposal.js.map +1 -0
- package/dist/lib/server/handlers/skills.d.ts +22 -0
- package/dist/lib/server/handlers/skills.d.ts.map +1 -0
- package/dist/lib/server/handlers/skills.js +119 -0
- package/dist/lib/server/handlers/skills.js.map +1 -0
- package/dist/lib/server-directory.d.ts +85 -0
- package/dist/lib/server-directory.d.ts.map +1 -0
- package/dist/lib/server-directory.js +177 -0
- package/dist/lib/server-directory.js.map +1 -0
- package/dist/lib/server.d.ts +162 -0
- package/dist/lib/server.d.ts.map +1 -0
- package/dist/lib/server.js +602 -0
- package/dist/lib/server.js.map +1 -0
- package/dist/lib/types.d.ts +461 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +98 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +22 -13
- package/bin/agentchat.js +0 -1617
- package/lib/chat.py +0 -241
- package/lib/client.js +0 -821
- package/lib/daemon.js +0 -562
- package/lib/deploy/akash.js +0 -811
- package/lib/deploy/config.js +0 -128
- package/lib/deploy/docker.js +0 -132
- package/lib/deploy/index.js +0 -24
- package/lib/elo_swarm.py +0 -569
- package/lib/escrow-hooks.js +0 -237
- package/lib/identity.js +0 -376
- package/lib/proposals.js +0 -426
- package/lib/protocol.js +0 -484
- package/lib/receipts.js +0 -294
- package/lib/reputation.js +0 -664
- package/lib/security.js +0 -183
- package/lib/server/handlers/identity.js +0 -242
- package/lib/server/handlers/index.js +0 -42
- package/lib/server/handlers/message.js +0 -306
- package/lib/server/handlers/presence.js +0 -44
- package/lib/server/handlers/proposal.js +0 -358
- package/lib/server/handlers/skills.js +0 -141
- package/lib/server-directory.js +0 -181
- package/lib/server.js +0 -528
- package/lib/supervisor/USAGE.md +0 -110
- package/lib/supervisor/agent-supervisor.sh +0 -135
- package/lib/supervisor/agentctl.sh +0 -250
- package/lib/supervisor/killswitch.sh +0 -36
- package/lib/supervisor/notify.sh +0 -19
package/lib/elo_swarm.py
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
ELO Swarm Simulation
|
|
4
|
-
Empirically validates our cooperative ELO system with staking.
|
|
5
|
-
|
|
6
|
-
Tests:
|
|
7
|
-
1. Does halving gains control inflation vs full gains?
|
|
8
|
-
2. Do reliable agents rise and unreliable agents fall?
|
|
9
|
-
3. Does staking create meaningful differentiation?
|
|
10
|
-
4. What's the equilibrium distribution?
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import random
|
|
14
|
-
import math
|
|
15
|
-
import statistics
|
|
16
|
-
from dataclasses import dataclass, field
|
|
17
|
-
from typing import List, Dict, Tuple, Optional
|
|
18
|
-
from collections import defaultdict
|
|
19
|
-
import json
|
|
20
|
-
|
|
21
|
-
# ============ ELO CONSTANTS (matching our implementation) ============
|
|
22
|
-
DEFAULT_RATING = 1200
|
|
23
|
-
MINIMUM_RATING = 100
|
|
24
|
-
ELO_DIVISOR = 400
|
|
25
|
-
|
|
26
|
-
# K-factor thresholds
|
|
27
|
-
K_NEW = 32 # < 30 transactions
|
|
28
|
-
K_INTERMEDIATE = 24 # < 100 transactions
|
|
29
|
-
K_ESTABLISHED = 16 # >= 100 transactions
|
|
30
|
-
|
|
31
|
-
TRANSACTIONS_NEW = 30
|
|
32
|
-
TRANSACTIONS_INTERMEDIATE = 100
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# ============ ELO CALCULATIONS ============
|
|
36
|
-
def calculate_expected(self_rating: float, opponent_rating: float) -> float:
|
|
37
|
-
"""Standard ELO expected outcome"""
|
|
38
|
-
exponent = (opponent_rating - self_rating) / ELO_DIVISOR
|
|
39
|
-
return 1 / (1 + math.pow(10, exponent))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def get_k_factor(transactions: int) -> int:
|
|
43
|
-
"""K-factor based on experience"""
|
|
44
|
-
if transactions < TRANSACTIONS_NEW:
|
|
45
|
-
return K_NEW
|
|
46
|
-
elif transactions < TRANSACTIONS_INTERMEDIATE:
|
|
47
|
-
return K_INTERMEDIATE
|
|
48
|
-
return K_ESTABLISHED
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def calculate_completion_gain(self_rating: float, opponent_rating: float, k: int, halve: bool = True) -> int:
|
|
52
|
-
"""Gain from successful completion"""
|
|
53
|
-
expected = calculate_expected(self_rating, opponent_rating)
|
|
54
|
-
gain = k * (1 - expected)
|
|
55
|
-
if halve:
|
|
56
|
-
gain = gain / 2
|
|
57
|
-
return max(1, round(gain))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def calculate_dispute_loss(self_rating: float, opponent_rating: float, k: int) -> int:
|
|
61
|
-
"""Loss from dispute (at-fault party)"""
|
|
62
|
-
expected = calculate_expected(self_rating, opponent_rating)
|
|
63
|
-
loss = k * expected
|
|
64
|
-
return max(1, round(loss))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# ============ AGENT TYPES ============
|
|
68
|
-
@dataclass
|
|
69
|
-
class Agent:
|
|
70
|
-
id: int
|
|
71
|
-
agent_type: str # 'reliable', 'unreliable', 'malicious', 'selective'
|
|
72
|
-
rating: float = DEFAULT_RATING
|
|
73
|
-
transactions: int = 0
|
|
74
|
-
completions: int = 0
|
|
75
|
-
disputes_won: int = 0
|
|
76
|
-
disputes_lost: int = 0
|
|
77
|
-
escrowed: float = 0
|
|
78
|
-
|
|
79
|
-
# Behavioral parameters
|
|
80
|
-
reliability: float = 0.9 # Probability of completing successfully
|
|
81
|
-
stake_willingness: float = 0.5 # How much of available ELO to stake
|
|
82
|
-
|
|
83
|
-
def available_elo(self) -> float:
|
|
84
|
-
return max(0, self.rating - self.escrowed - MINIMUM_RATING)
|
|
85
|
-
|
|
86
|
-
def get_stake(self, max_stake: float = 100) -> float:
|
|
87
|
-
available = self.available_elo()
|
|
88
|
-
desired = available * self.stake_willingness
|
|
89
|
-
return min(desired, max_stake)
|
|
90
|
-
|
|
91
|
-
def will_complete(self, counterparty: 'Agent') -> bool:
|
|
92
|
-
"""Decide whether to complete based on agent type"""
|
|
93
|
-
if self.agent_type == 'reliable':
|
|
94
|
-
return random.random() < self.reliability
|
|
95
|
-
elif self.agent_type == 'unreliable':
|
|
96
|
-
return random.random() < self.reliability
|
|
97
|
-
elif self.agent_type == 'malicious':
|
|
98
|
-
# Malicious agents defect more against high-rated agents
|
|
99
|
-
defect_prob = 0.5 + (counterparty.rating - 1200) / 2000
|
|
100
|
-
return random.random() > defect_prob
|
|
101
|
-
elif self.agent_type == 'selective':
|
|
102
|
-
# Only complete with agents rated within 200 points
|
|
103
|
-
if abs(self.rating - counterparty.rating) > 200:
|
|
104
|
-
return random.random() < 0.5
|
|
105
|
-
return random.random() < 0.95
|
|
106
|
-
return random.random() < 0.8
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@dataclass
|
|
110
|
-
class SimulationResult:
|
|
111
|
-
rounds: int
|
|
112
|
-
agents: List[Agent]
|
|
113
|
-
rating_history: List[Dict[int, float]]
|
|
114
|
-
total_completions: int
|
|
115
|
-
total_disputes: int
|
|
116
|
-
inflation_rate: float
|
|
117
|
-
gini_coefficient: float
|
|
118
|
-
type_avg_ratings: Dict[str, float]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ============ SIMULATION ============
|
|
122
|
-
class ELOSimulation:
|
|
123
|
-
def __init__(
|
|
124
|
-
self,
|
|
125
|
-
n_reliable: int = 50,
|
|
126
|
-
n_unreliable: int = 20,
|
|
127
|
-
n_malicious: int = 10,
|
|
128
|
-
n_selective: int = 20,
|
|
129
|
-
halve_gains: bool = True,
|
|
130
|
-
enable_staking: bool = True
|
|
131
|
-
):
|
|
132
|
-
self.halve_gains = halve_gains
|
|
133
|
-
self.enable_staking = enable_staking
|
|
134
|
-
self.agents: List[Agent] = []
|
|
135
|
-
self.rating_history: List[Dict[int, float]] = []
|
|
136
|
-
self.total_completions = 0
|
|
137
|
-
self.total_disputes = 0
|
|
138
|
-
|
|
139
|
-
# Create agents
|
|
140
|
-
agent_id = 0
|
|
141
|
-
for _ in range(n_reliable):
|
|
142
|
-
self.agents.append(Agent(
|
|
143
|
-
id=agent_id,
|
|
144
|
-
agent_type='reliable',
|
|
145
|
-
reliability=random.uniform(0.85, 0.99),
|
|
146
|
-
stake_willingness=random.uniform(0.3, 0.7)
|
|
147
|
-
))
|
|
148
|
-
agent_id += 1
|
|
149
|
-
|
|
150
|
-
for _ in range(n_unreliable):
|
|
151
|
-
self.agents.append(Agent(
|
|
152
|
-
id=agent_id,
|
|
153
|
-
agent_type='unreliable',
|
|
154
|
-
reliability=random.uniform(0.3, 0.6),
|
|
155
|
-
stake_willingness=random.uniform(0.1, 0.3)
|
|
156
|
-
))
|
|
157
|
-
agent_id += 1
|
|
158
|
-
|
|
159
|
-
for _ in range(n_malicious):
|
|
160
|
-
self.agents.append(Agent(
|
|
161
|
-
id=agent_id,
|
|
162
|
-
agent_type='malicious',
|
|
163
|
-
reliability=0.3,
|
|
164
|
-
stake_willingness=random.uniform(0.0, 0.2)
|
|
165
|
-
))
|
|
166
|
-
agent_id += 1
|
|
167
|
-
|
|
168
|
-
for _ in range(n_selective):
|
|
169
|
-
self.agents.append(Agent(
|
|
170
|
-
id=agent_id,
|
|
171
|
-
agent_type='selective',
|
|
172
|
-
reliability=0.9,
|
|
173
|
-
stake_willingness=random.uniform(0.4, 0.8)
|
|
174
|
-
))
|
|
175
|
-
agent_id += 1
|
|
176
|
-
|
|
177
|
-
def record_ratings(self):
|
|
178
|
-
"""Snapshot current ratings"""
|
|
179
|
-
self.rating_history.append({
|
|
180
|
-
a.id: a.rating for a in self.agents
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
def process_interaction(self, proposer: Agent, acceptor: Agent):
|
|
184
|
-
"""Process a single proposal interaction"""
|
|
185
|
-
# Determine stakes
|
|
186
|
-
proposer_stake = proposer.get_stake() if self.enable_staking else 0
|
|
187
|
-
acceptor_stake = acceptor.get_stake() if self.enable_staking else 0
|
|
188
|
-
|
|
189
|
-
# Escrow stakes
|
|
190
|
-
proposer.escrowed += proposer_stake
|
|
191
|
-
acceptor.escrowed += acceptor_stake
|
|
192
|
-
|
|
193
|
-
# Determine outcome
|
|
194
|
-
proposer_completes = proposer.will_complete(acceptor)
|
|
195
|
-
acceptor_completes = acceptor.will_complete(proposer)
|
|
196
|
-
|
|
197
|
-
k_proposer = get_k_factor(proposer.transactions)
|
|
198
|
-
k_acceptor = get_k_factor(acceptor.transactions)
|
|
199
|
-
|
|
200
|
-
if proposer_completes and acceptor_completes:
|
|
201
|
-
# COMPLETE - both gain (halved), stakes returned
|
|
202
|
-
gain_proposer = calculate_completion_gain(
|
|
203
|
-
proposer.rating, acceptor.rating, k_proposer, self.halve_gains
|
|
204
|
-
)
|
|
205
|
-
gain_acceptor = calculate_completion_gain(
|
|
206
|
-
acceptor.rating, proposer.rating, k_acceptor, self.halve_gains
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
proposer.rating = max(MINIMUM_RATING, proposer.rating + gain_proposer)
|
|
210
|
-
acceptor.rating = max(MINIMUM_RATING, acceptor.rating + gain_acceptor)
|
|
211
|
-
proposer.completions += 1
|
|
212
|
-
acceptor.completions += 1
|
|
213
|
-
self.total_completions += 1
|
|
214
|
-
|
|
215
|
-
# Return stakes
|
|
216
|
-
proposer.escrowed -= proposer_stake
|
|
217
|
-
acceptor.escrowed -= acceptor_stake
|
|
218
|
-
|
|
219
|
-
elif not proposer_completes and not acceptor_completes:
|
|
220
|
-
# MUTUAL FAULT - both lose, both stakes burned
|
|
221
|
-
loss_proposer = calculate_dispute_loss(
|
|
222
|
-
proposer.rating, acceptor.rating, k_proposer
|
|
223
|
-
) + proposer_stake
|
|
224
|
-
loss_acceptor = calculate_dispute_loss(
|
|
225
|
-
acceptor.rating, proposer.rating, k_acceptor
|
|
226
|
-
) + acceptor_stake
|
|
227
|
-
|
|
228
|
-
proposer.rating = max(MINIMUM_RATING, proposer.rating - loss_proposer)
|
|
229
|
-
acceptor.rating = max(MINIMUM_RATING, acceptor.rating - loss_acceptor)
|
|
230
|
-
proposer.disputes_lost += 1
|
|
231
|
-
acceptor.disputes_lost += 1
|
|
232
|
-
self.total_disputes += 1
|
|
233
|
-
|
|
234
|
-
# Stakes burned (already escrowed, just clear)
|
|
235
|
-
proposer.escrowed -= proposer_stake
|
|
236
|
-
acceptor.escrowed -= acceptor_stake
|
|
237
|
-
|
|
238
|
-
else:
|
|
239
|
-
# ONE PARTY AT FAULT - winner gains some, loser loses + stake transfer
|
|
240
|
-
if proposer_completes:
|
|
241
|
-
# Acceptor at fault
|
|
242
|
-
winner, loser = proposer, acceptor
|
|
243
|
-
winner_stake, loser_stake = proposer_stake, acceptor_stake
|
|
244
|
-
k_winner, k_loser = k_proposer, k_acceptor
|
|
245
|
-
else:
|
|
246
|
-
# Proposer at fault
|
|
247
|
-
winner, loser = acceptor, proposer
|
|
248
|
-
winner_stake, loser_stake = acceptor_stake, proposer_stake
|
|
249
|
-
k_winner, k_loser = k_acceptor, k_proposer
|
|
250
|
-
|
|
251
|
-
# Winner gains partial + loser's stake
|
|
252
|
-
win_gain = round(calculate_dispute_loss(loser.rating, winner.rating, k_loser) * 0.5)
|
|
253
|
-
winner.rating = max(MINIMUM_RATING, winner.rating + win_gain + loser_stake)
|
|
254
|
-
|
|
255
|
-
# Loser loses full + their stake
|
|
256
|
-
lose_loss = calculate_dispute_loss(loser.rating, winner.rating, k_loser) + loser_stake
|
|
257
|
-
loser.rating = max(MINIMUM_RATING, loser.rating - lose_loss)
|
|
258
|
-
|
|
259
|
-
winner.disputes_won += 1
|
|
260
|
-
loser.disputes_lost += 1
|
|
261
|
-
self.total_disputes += 1
|
|
262
|
-
|
|
263
|
-
# Clear escrows
|
|
264
|
-
proposer.escrowed -= proposer_stake
|
|
265
|
-
acceptor.escrowed -= acceptor_stake
|
|
266
|
-
|
|
267
|
-
# Update transaction counts
|
|
268
|
-
proposer.transactions += 1
|
|
269
|
-
acceptor.transactions += 1
|
|
270
|
-
|
|
271
|
-
def run(self, rounds: int = 1000, interactions_per_round: int = 50) -> SimulationResult:
|
|
272
|
-
"""Run the simulation"""
|
|
273
|
-
self.record_ratings()
|
|
274
|
-
|
|
275
|
-
for round_num in range(rounds):
|
|
276
|
-
# Random pairings for this round
|
|
277
|
-
for _ in range(interactions_per_round):
|
|
278
|
-
# Select two different agents (weighted by rating for "market" effect)
|
|
279
|
-
weights = [max(1, a.rating - MINIMUM_RATING) for a in self.agents]
|
|
280
|
-
proposer, acceptor = random.choices(self.agents, weights=weights, k=2)
|
|
281
|
-
|
|
282
|
-
# Ensure different agents
|
|
283
|
-
while proposer.id == acceptor.id:
|
|
284
|
-
acceptor = random.choices(self.agents, weights=weights, k=1)[0]
|
|
285
|
-
|
|
286
|
-
self.process_interaction(proposer, acceptor)
|
|
287
|
-
|
|
288
|
-
# Record every 10 rounds
|
|
289
|
-
if round_num % 10 == 0:
|
|
290
|
-
self.record_ratings()
|
|
291
|
-
|
|
292
|
-
# Final snapshot
|
|
293
|
-
self.record_ratings()
|
|
294
|
-
|
|
295
|
-
return self.get_results(rounds)
|
|
296
|
-
|
|
297
|
-
def get_results(self, rounds: int) -> SimulationResult:
|
|
298
|
-
"""Compile simulation results"""
|
|
299
|
-
# Calculate inflation
|
|
300
|
-
initial_total = sum(self.rating_history[0].values())
|
|
301
|
-
final_total = sum(a.rating for a in self.agents)
|
|
302
|
-
inflation_rate = (final_total - initial_total) / initial_total
|
|
303
|
-
|
|
304
|
-
# Calculate Gini coefficient (inequality measure)
|
|
305
|
-
ratings = sorted([a.rating for a in self.agents])
|
|
306
|
-
n = len(ratings)
|
|
307
|
-
cumulative = sum((i + 1) * r for i, r in enumerate(ratings))
|
|
308
|
-
gini = (2 * cumulative) / (n * sum(ratings)) - (n + 1) / n
|
|
309
|
-
|
|
310
|
-
# Average rating by type
|
|
311
|
-
type_ratings = defaultdict(list)
|
|
312
|
-
for a in self.agents:
|
|
313
|
-
type_ratings[a.agent_type].append(a.rating)
|
|
314
|
-
|
|
315
|
-
type_avg = {t: statistics.mean(rs) for t, rs in type_ratings.items()}
|
|
316
|
-
|
|
317
|
-
return SimulationResult(
|
|
318
|
-
rounds=rounds,
|
|
319
|
-
agents=self.agents,
|
|
320
|
-
rating_history=self.rating_history,
|
|
321
|
-
total_completions=self.total_completions,
|
|
322
|
-
total_disputes=self.total_disputes,
|
|
323
|
-
inflation_rate=inflation_rate,
|
|
324
|
-
gini_coefficient=gini,
|
|
325
|
-
type_avg_ratings=type_avg
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def print_results(result: SimulationResult, label: str):
|
|
330
|
-
"""Pretty print simulation results"""
|
|
331
|
-
print(f"\n{'='*60}")
|
|
332
|
-
print(f" {label}")
|
|
333
|
-
print(f"{'='*60}")
|
|
334
|
-
print(f"Rounds: {result.rounds}")
|
|
335
|
-
print(f"Completions: {result.total_completions}, Disputes: {result.total_disputes}")
|
|
336
|
-
print(f"Completion Rate: {result.total_completions / (result.total_completions + result.total_disputes):.1%}")
|
|
337
|
-
print(f"\nInflation Rate: {result.inflation_rate:+.2%}")
|
|
338
|
-
print(f"Gini Coefficient: {result.gini_coefficient:.3f} (0=equal, 1=unequal)")
|
|
339
|
-
|
|
340
|
-
print(f"\nAverage Rating by Type:")
|
|
341
|
-
for agent_type, avg in sorted(result.type_avg_ratings.items(), key=lambda x: -x[1]):
|
|
342
|
-
print(f" {agent_type:12s}: {avg:.0f}")
|
|
343
|
-
|
|
344
|
-
# Top 10 agents
|
|
345
|
-
top_agents = sorted(result.agents, key=lambda a: -a.rating)[:10]
|
|
346
|
-
print(f"\nTop 10 Agents:")
|
|
347
|
-
for i, a in enumerate(top_agents, 1):
|
|
348
|
-
print(f" {i:2d}. Agent {a.id:3d} ({a.agent_type:10s}): {a.rating:.0f} "
|
|
349
|
-
f"[{a.completions}C/{a.disputes_won}W/{a.disputes_lost}L]")
|
|
350
|
-
|
|
351
|
-
# Bottom 10 agents
|
|
352
|
-
bottom_agents = sorted(result.agents, key=lambda a: a.rating)[:10]
|
|
353
|
-
print(f"\nBottom 10 Agents:")
|
|
354
|
-
for i, a in enumerate(bottom_agents, 1):
|
|
355
|
-
print(f" {i:2d}. Agent {a.id:3d} ({a.agent_type:10s}): {a.rating:.0f} "
|
|
356
|
-
f"[{a.completions}C/{a.disputes_won}W/{a.disputes_lost}L]")
|
|
357
|
-
|
|
358
|
-
# Rating distribution
|
|
359
|
-
ratings = [a.rating for a in result.agents]
|
|
360
|
-
print(f"\nRating Distribution:")
|
|
361
|
-
print(f" Min: {min(ratings):.0f}, Max: {max(ratings):.0f}")
|
|
362
|
-
print(f" Mean: {statistics.mean(ratings):.0f}, Median: {statistics.median(ratings):.0f}")
|
|
363
|
-
print(f" Std Dev: {statistics.stdev(ratings):.0f}")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def analyze_dynamics(result: SimulationResult):
|
|
367
|
-
"""Analyze rating dynamics over time"""
|
|
368
|
-
print(f"\nRating Evolution (sampled):")
|
|
369
|
-
|
|
370
|
-
# Get snapshots at 0%, 25%, 50%, 75%, 100%
|
|
371
|
-
n_snapshots = len(result.rating_history)
|
|
372
|
-
indices = [0, n_snapshots//4, n_snapshots//2, 3*n_snapshots//4, n_snapshots-1]
|
|
373
|
-
|
|
374
|
-
# Track by type
|
|
375
|
-
type_ids = defaultdict(list)
|
|
376
|
-
for a in result.agents:
|
|
377
|
-
type_ids[a.agent_type].append(a.id)
|
|
378
|
-
|
|
379
|
-
print(f"\n{'Progress':<10}", end="")
|
|
380
|
-
for t in ['reliable', 'unreliable', 'malicious', 'selective']:
|
|
381
|
-
print(f"{t:>12}", end="")
|
|
382
|
-
print()
|
|
383
|
-
print("-" * 60)
|
|
384
|
-
|
|
385
|
-
for idx in indices:
|
|
386
|
-
snapshot = result.rating_history[idx]
|
|
387
|
-
progress = idx / (n_snapshots - 1) * 100
|
|
388
|
-
print(f"{progress:>6.0f}% ", end="")
|
|
389
|
-
for t in ['reliable', 'unreliable', 'malicious', 'selective']:
|
|
390
|
-
avg = statistics.mean(snapshot[aid] for aid in type_ids[t])
|
|
391
|
-
print(f"{avg:>12.0f}", end="")
|
|
392
|
-
print()
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def run_comparison():
|
|
396
|
-
"""Compare different configurations"""
|
|
397
|
-
random.seed(42) # Reproducibility
|
|
398
|
-
|
|
399
|
-
print("\n" + "="*70)
|
|
400
|
-
print(" ELO SWARM SIMULATION - EMPIRICAL VALIDATION")
|
|
401
|
-
print("="*70)
|
|
402
|
-
print("\nAgent Distribution: 50 reliable, 20 unreliable, 10 malicious, 20 selective")
|
|
403
|
-
print("Running 1000 rounds with 50 interactions each (50,000 total interactions)")
|
|
404
|
-
|
|
405
|
-
# Test 1: Full gains (no halving) - should show inflation
|
|
406
|
-
print("\n[1/4] Running: Full Gains (no halving), No Staking...")
|
|
407
|
-
sim1 = ELOSimulation(halve_gains=False, enable_staking=False)
|
|
408
|
-
result1 = sim1.run(rounds=1000)
|
|
409
|
-
print_results(result1, "FULL GAINS, NO STAKING (baseline)")
|
|
410
|
-
|
|
411
|
-
# Test 2: Halved gains - should control inflation
|
|
412
|
-
print("\n[2/4] Running: Halved Gains, No Staking...")
|
|
413
|
-
random.seed(42)
|
|
414
|
-
sim2 = ELOSimulation(halve_gains=True, enable_staking=False)
|
|
415
|
-
result2 = sim2.run(rounds=1000)
|
|
416
|
-
print_results(result2, "HALVED GAINS, NO STAKING")
|
|
417
|
-
|
|
418
|
-
# Test 3: Halved gains + staking
|
|
419
|
-
print("\n[3/4] Running: Halved Gains + Staking...")
|
|
420
|
-
random.seed(42)
|
|
421
|
-
sim3 = ELOSimulation(halve_gains=True, enable_staking=True)
|
|
422
|
-
result3 = sim3.run(rounds=1000)
|
|
423
|
-
print_results(result3, "HALVED GAINS + STAKING (our system)")
|
|
424
|
-
|
|
425
|
-
# Test 4: Full gains + staking (for comparison)
|
|
426
|
-
print("\n[4/4] Running: Full Gains + Staking...")
|
|
427
|
-
random.seed(42)
|
|
428
|
-
sim4 = ELOSimulation(halve_gains=False, enable_staking=True)
|
|
429
|
-
result4 = sim4.run(rounds=1000)
|
|
430
|
-
print_results(result4, "FULL GAINS + STAKING")
|
|
431
|
-
|
|
432
|
-
# Summary comparison
|
|
433
|
-
print("\n" + "="*70)
|
|
434
|
-
print(" SUMMARY COMPARISON")
|
|
435
|
-
print("="*70)
|
|
436
|
-
print(f"\n{'Configuration':<35} {'Inflation':>12} {'Gini':>8} {'Reliable Avg':>14}")
|
|
437
|
-
print("-"*70)
|
|
438
|
-
configs = [
|
|
439
|
-
("Full Gains, No Staking", result1),
|
|
440
|
-
("Halved Gains, No Staking", result2),
|
|
441
|
-
("Halved Gains + Staking (OURS)", result3),
|
|
442
|
-
("Full Gains + Staking", result4),
|
|
443
|
-
]
|
|
444
|
-
for label, r in configs:
|
|
445
|
-
reliable_avg = r.type_avg_ratings.get('reliable', 0)
|
|
446
|
-
print(f"{label:<35} {r.inflation_rate:>+11.1%} {r.gini_coefficient:>8.3f} {reliable_avg:>14.0f}")
|
|
447
|
-
|
|
448
|
-
print("\n" + "="*70)
|
|
449
|
-
print(" KEY FINDINGS")
|
|
450
|
-
print("="*70)
|
|
451
|
-
|
|
452
|
-
# Analyze findings
|
|
453
|
-
inflation_controlled = result3.inflation_rate < result1.inflation_rate * 0.5
|
|
454
|
-
reliable_on_top = result3.type_avg_ratings['reliable'] > result3.type_avg_ratings['malicious']
|
|
455
|
-
staking_differentiates = result3.gini_coefficient > result2.gini_coefficient
|
|
456
|
-
|
|
457
|
-
print(f"\n1. Inflation Control: {'✓ PASS' if inflation_controlled else '✗ FAIL'}")
|
|
458
|
-
print(f" Full gains: {result1.inflation_rate:+.1%} → Halved: {result3.inflation_rate:+.1%}")
|
|
459
|
-
|
|
460
|
-
print(f"\n2. Reliable Agents Rise: {'✓ PASS' if reliable_on_top else '✗ FAIL'}")
|
|
461
|
-
print(f" Reliable avg: {result3.type_avg_ratings['reliable']:.0f}")
|
|
462
|
-
print(f" Malicious avg: {result3.type_avg_ratings['malicious']:.0f}")
|
|
463
|
-
print(f" Unreliable avg: {result3.type_avg_ratings['unreliable']:.0f}")
|
|
464
|
-
|
|
465
|
-
print(f"\n3. Staking Creates Differentiation: {'✓ PASS' if staking_differentiates else '✗ FAIL'}")
|
|
466
|
-
print(f" Gini without staking: {result2.gini_coefficient:.3f}")
|
|
467
|
-
print(f" Gini with staking: {result3.gini_coefficient:.3f}")
|
|
468
|
-
|
|
469
|
-
malicious_bottom = sum(1 for a in sorted(result3.agents, key=lambda x: x.rating)[:20]
|
|
470
|
-
if a.agent_type == 'malicious')
|
|
471
|
-
print(f"\n4. Malicious Agents Sink: {malicious_bottom}/10 malicious agents in bottom 20")
|
|
472
|
-
|
|
473
|
-
# Show dynamics for our system
|
|
474
|
-
print("\n" + "="*70)
|
|
475
|
-
print(" RATING DYNAMICS OVER TIME (Our System)")
|
|
476
|
-
print("="*70)
|
|
477
|
-
analyze_dynamics(result3)
|
|
478
|
-
|
|
479
|
-
# Mathematical analysis
|
|
480
|
-
print("\n" + "="*70)
|
|
481
|
-
print(" MATHEMATICAL ANALYSIS")
|
|
482
|
-
print("="*70)
|
|
483
|
-
|
|
484
|
-
# Expected value analysis
|
|
485
|
-
print("\nExpected ELO change per interaction (at equilibrium):")
|
|
486
|
-
print(" If P(complete) = p, P(dispute) = 1-p")
|
|
487
|
-
print(" E[gain|complete] ≈ K/2 * (1 - 0.5) = K/4 (halved)")
|
|
488
|
-
print(" E[loss|dispute] ≈ K * 0.5 = K/2")
|
|
489
|
-
print(" E[change] = p * K/4 - (1-p) * K/2")
|
|
490
|
-
print(" Break-even: p * K/4 = (1-p) * K/2")
|
|
491
|
-
print(" p = 2(1-p)")
|
|
492
|
-
print(" p = 2/3 ≈ 66.7% completion rate needed to break even")
|
|
493
|
-
|
|
494
|
-
actual_rate = result3.total_completions / (result3.total_completions + result3.total_disputes)
|
|
495
|
-
print(f"\n Actual completion rate: {actual_rate:.1%}")
|
|
496
|
-
print(f" System is {'inflationary' if actual_rate > 0.667 else 'deflationary'} at this rate")
|
|
497
|
-
|
|
498
|
-
# Stake impact
|
|
499
|
-
print("\nStake Impact Analysis:")
|
|
500
|
-
reliable_with_stake = [a for a in result3.agents if a.agent_type == 'reliable']
|
|
501
|
-
avg_stake_willing = statistics.mean(a.stake_willingness for a in reliable_with_stake)
|
|
502
|
-
print(f" Avg stake willingness (reliable): {avg_stake_willing:.1%}")
|
|
503
|
-
print(f" Stake amplifies differentiation by transferring ELO from losers to winners")
|
|
504
|
-
print(f" Gini increase: {result2.gini_coefficient:.3f} → {result3.gini_coefficient:.3f}")
|
|
505
|
-
print(f" This is {(result3.gini_coefficient/result2.gini_coefficient - 1)*100:.0f}% more unequal (intended)")
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def run_equilibrium_analysis():
|
|
509
|
-
"""Find the equilibrium completion rate that balances inflation"""
|
|
510
|
-
print("\n" + "="*70)
|
|
511
|
-
print(" EQUILIBRIUM ANALYSIS")
|
|
512
|
-
print("="*70)
|
|
513
|
-
|
|
514
|
-
print("\nTesting different population mixes to find equilibrium...")
|
|
515
|
-
print(f"\n{'Mix':<30} {'Completion%':>12} {'Inflation':>12} {'Reliable Avg':>14}")
|
|
516
|
-
print("-" * 70)
|
|
517
|
-
|
|
518
|
-
configs = [
|
|
519
|
-
("High reliability (70/10/5/15)", (70, 10, 5, 15)),
|
|
520
|
-
("Our mix (50/20/10/20)", (50, 20, 10, 20)),
|
|
521
|
-
("Adversarial (30/30/20/20)", (30, 30, 20, 20)),
|
|
522
|
-
("Very adversarial (20/30/30/20)", (20, 30, 30, 20)),
|
|
523
|
-
]
|
|
524
|
-
|
|
525
|
-
for label, (rel, unrel, mal, sel) in configs:
|
|
526
|
-
random.seed(42)
|
|
527
|
-
sim = ELOSimulation(
|
|
528
|
-
n_reliable=rel,
|
|
529
|
-
n_unreliable=unrel,
|
|
530
|
-
n_malicious=mal,
|
|
531
|
-
n_selective=sel,
|
|
532
|
-
halve_gains=True,
|
|
533
|
-
enable_staking=True
|
|
534
|
-
)
|
|
535
|
-
result = sim.run(rounds=500, interactions_per_round=30)
|
|
536
|
-
comp_rate = result.total_completions / (result.total_completions + result.total_disputes)
|
|
537
|
-
rel_avg = result.type_avg_ratings.get('reliable', 0)
|
|
538
|
-
print(f"{label:<30} {comp_rate:>11.1%} {result.inflation_rate:>+11.1%} {rel_avg:>14.0f}")
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
def run_long_term_stability():
|
|
542
|
-
"""Test long-term stability of the system"""
|
|
543
|
-
print("\n" + "="*70)
|
|
544
|
-
print(" LONG-TERM STABILITY TEST")
|
|
545
|
-
print("="*70)
|
|
546
|
-
|
|
547
|
-
random.seed(42)
|
|
548
|
-
sim = ELOSimulation(halve_gains=True, enable_staking=True)
|
|
549
|
-
result = sim.run(rounds=2000, interactions_per_round=50)
|
|
550
|
-
|
|
551
|
-
print(f"\nAfter 2000 rounds (100,000 interactions):")
|
|
552
|
-
print(f" Completion rate: {result.total_completions / (result.total_completions + result.total_disputes):.1%}")
|
|
553
|
-
print(f" Inflation: {result.inflation_rate:+.1%}")
|
|
554
|
-
print(f" Gini: {result.gini_coefficient:.3f}")
|
|
555
|
-
print(f"\n Type averages:")
|
|
556
|
-
for t, avg in sorted(result.type_avg_ratings.items(), key=lambda x: -x[1]):
|
|
557
|
-
print(f" {t}: {avg:.0f}")
|
|
558
|
-
|
|
559
|
-
# Check if rankings are stable
|
|
560
|
-
top_10_ids = [a.id for a in sorted(result.agents, key=lambda x: -x.rating)[:10]]
|
|
561
|
-
top_types = [a.agent_type for a in sorted(result.agents, key=lambda x: -x.rating)[:10]]
|
|
562
|
-
print(f"\n Top 10 agent types: {', '.join(top_types)}")
|
|
563
|
-
print(f" All top 10 are reliable: {all(t == 'reliable' for t in top_types)}")
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if __name__ == "__main__":
|
|
567
|
-
run_comparison()
|
|
568
|
-
run_equilibrium_analysis()
|
|
569
|
-
run_long_term_stability()
|