narcolepticchicken commited on
Commit
a98a7f5
·
verified ·
1 Parent(s): b8754a6

Upload ledger/ledger.py

Browse files
Files changed (1) hide show
  1. ledger/ledger.py +342 -0
ledger/ledger.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Credit Ledger: non-transferable, decaying, capability-scoped credits with provenance.
3
+ """
4
+
5
+ import json
6
+ import math
7
+ import time
8
+ from dataclasses import asdict, dataclass, field
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Tuple
11
+
12
+
13
+ @dataclass
14
+ class LedgerEntry:
15
+ agent_id: str
16
+ task_id: str
17
+ action_id: str
18
+ earned_credit: float = 0.0
19
+ spent_credit: float = 0.0
20
+ decayed_credit: float = 0.0
21
+ remaining_credit: float = 0.0
22
+ reason: str = ""
23
+ oracle_score: float = 0.0
24
+ compute_cost: float = 0.0
25
+ timestamp: float = field(default_factory=time.time)
26
+ capability_scope: str = "general"
27
+ task_scope: str = "global"
28
+ provenance_hash: str = ""
29
+
30
+
31
+ class CreditLedger:
32
+ """
33
+ Immutable-style ledger with decay, non-transferability, and anti-gaming rules.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ decay_lambda: float = 0.05, # per-eval decay factor
39
+ hoarding_threshold: float = 100.0,
40
+ hoarding_window: int = 50,
41
+ max_history: int = 10000,
42
+ ledger_path: Optional[str] = None,
43
+ ):
44
+ self.decay_lambda = decay_lambda
45
+ self.hoarding_threshold = hoarding_threshold
46
+ self.hoarding_window = hoarding_window
47
+ self.max_history = max_history
48
+ self.ledger_path = ledger_path
49
+
50
+ # Main ledger: list of LedgerEntry
51
+ self.entries: List[LedgerEntry] = []
52
+
53
+ # Per-agent, per-scope balances (computed lazily)
54
+ self._agent_balances: Dict[Tuple[str, str, str], float] = {}
55
+
56
+ # Anti-gaming: track agent behavior patterns
57
+ self._agent_action_history: Dict[str, List[Dict]] = {}
58
+
59
+ # ------------------------------------------------------------------
60
+ # Core operations
61
+ # ------------------------------------------------------------------
62
+
63
+ def earn(
64
+ self,
65
+ agent_id: str,
66
+ task_id: str,
67
+ action_id: str,
68
+ amount: float,
69
+ oracle_score: float,
70
+ compute_cost: float,
71
+ reason: str,
72
+ capability_scope: str = "general",
73
+ task_scope: str = "global",
74
+ provenance_hash: str = "",
75
+ ) -> LedgerEntry:
76
+ """Award credits based on verified impact."""
77
+ if amount < 0:
78
+ raise ValueError("Earn amount must be non-negative")
79
+
80
+ # Apply hoarding penalty: if agent has held high balance without spending
81
+ current_balance = self.balance(agent_id, capability_scope, task_scope)
82
+ if current_balance > self.hoarding_threshold:
83
+ # Accelerated decay for hoarders
84
+ amount *= 0.8
85
+ reason += " [hoarding_penalty_applied]"
86
+
87
+ entry = LedgerEntry(
88
+ agent_id=agent_id,
89
+ task_id=task_id,
90
+ action_id=action_id,
91
+ earned_credit=amount,
92
+ spent_credit=0.0,
93
+ decayed_credit=0.0,
94
+ remaining_credit=amount,
95
+ reason=reason,
96
+ oracle_score=oracle_score,
97
+ compute_cost=compute_cost,
98
+ capability_scope=capability_scope,
99
+ task_scope=task_scope,
100
+ provenance_hash=provenance_hash,
101
+ )
102
+ self.entries.append(entry)
103
+ self._trim_history()
104
+ self._invalidate_cache(agent_id, capability_scope, task_scope)
105
+ return entry
106
+
107
+ def spend(
108
+ self,
109
+ agent_id: str,
110
+ task_id: str,
111
+ action_id: str,
112
+ amount: float,
113
+ capability_scope: str = "general",
114
+ task_scope: str = "global",
115
+ reason: str = "",
116
+ ) -> Tuple[bool, LedgerEntry]:
117
+ """Spend credits. Returns (success, entry)."""
118
+ if amount < 0:
119
+ return False, None
120
+
121
+ self._apply_decay(agent_id, capability_scope, task_scope)
122
+ current = self.balance(agent_id, capability_scope, task_scope)
123
+
124
+ if current < amount:
125
+ return False, None
126
+
127
+ entry = LedgerEntry(
128
+ agent_id=agent_id,
129
+ task_id=task_id,
130
+ action_id=action_id,
131
+ earned_credit=0.0,
132
+ spent_credit=amount,
133
+ decayed_credit=0.0,
134
+ remaining_credit=current - amount,
135
+ reason=reason,
136
+ capability_scope=capability_scope,
137
+ task_scope=task_scope,
138
+ )
139
+ self.entries.append(entry)
140
+ self._trim_history()
141
+ self._invalidate_cache(agent_id, capability_scope, task_scope)
142
+ return True, entry
143
+
144
+ def transfer(
145
+ self,
146
+ from_agent: str,
147
+ to_agent: str,
148
+ amount: float,
149
+ capability_scope: str = "general",
150
+ task_scope: str = "global",
151
+ ) -> bool:
152
+ """
153
+ Credits are NON-TRANSFERABLE by design.
154
+ Returns False always; logs an attempted transfer for audit.
155
+ """
156
+ entry = LedgerEntry(
157
+ agent_id=from_agent,
158
+ task_id="TRANSFER_ATTEMPT",
159
+ action_id=f"to:{to_agent}",
160
+ earned_credit=0.0,
161
+ spent_credit=0.0,
162
+ decayed_credit=0.0,
163
+ remaining_credit=self.balance(from_agent, capability_scope, task_scope),
164
+ reason=f"TRANSFER_BLOCKED: {amount} to {to_agent}",
165
+ capability_scope=capability_scope,
166
+ task_scope=task_scope,
167
+ )
168
+ self.entries.append(entry)
169
+ return False
170
+
171
+ def revoke(
172
+ self,
173
+ agent_id: str,
174
+ task_id: str,
175
+ action_id: str,
176
+ amount: float,
177
+ reason: str,
178
+ capability_scope: str = "general",
179
+ task_scope: str = "global",
180
+ ) -> bool:
181
+ """Revoke credits retroactively after negative outcome."""
182
+ self._apply_decay(agent_id, capability_scope, task_scope)
183
+ current = self.balance(agent_id, capability_scope, task_scope)
184
+ revoke_amount = min(amount, current)
185
+
186
+ if revoke_amount <= 0:
187
+ return False
188
+
189
+ entry = LedgerEntry(
190
+ agent_id=agent_id,
191
+ task_id=task_id,
192
+ action_id=action_id,
193
+ earned_credit=0.0,
194
+ spent_credit=0.0,
195
+ decayed_credit=revoke_amount,
196
+ remaining_credit=current - revoke_amount,
197
+ reason=f"REVOKE: {reason}",
198
+ capability_scope=capability_scope,
199
+ task_scope=task_scope,
200
+ )
201
+ self.entries.append(entry)
202
+ self._invalidate_cache(agent_id, capability_scope, task_scope)
203
+ return True
204
+
205
+ def balance(
206
+ self,
207
+ agent_id: str,
208
+ capability_scope: str = "general",
209
+ task_scope: str = "global",
210
+ ) -> float:
211
+ """Compute current balance after decay."""
212
+ self._apply_decay(agent_id, capability_scope, task_scope)
213
+ key = (agent_id, capability_scope, task_scope)
214
+ return self._agent_balances.get(key, 0.0)
215
+
216
+ # ------------------------------------------------------------------
217
+ # Decay
218
+ # ------------------------------------------------------------------
219
+
220
+ def _apply_decay(self, agent_id: str, capability_scope: str, task_scope: str):
221
+ """Apply exponential decay since last update."""
222
+ key = (agent_id, capability_scope, task_scope)
223
+ relevant = [
224
+ e for e in self.entries
225
+ if e.agent_id == agent_id
226
+ and e.capability_scope == capability_scope
227
+ and e.task_scope == task_scope
228
+ ]
229
+ if not relevant:
230
+ self._agent_balances[key] = 0.0
231
+ return
232
+
233
+ now = time.time()
234
+ # Find last balance-updating entry
235
+ last_time = relevant[-1].timestamp
236
+ delta_t = now - last_time
237
+
238
+ # Apply decay to cached balance
239
+ current = self._agent_balances.get(key, 0.0)
240
+ decayed = current * math.exp(-self.decay_lambda * delta_t)
241
+ if decayed != current:
242
+ entry = LedgerEntry(
243
+ agent_id=agent_id,
244
+ task_id="DECAY",
245
+ action_id="auto",
246
+ earned_credit=0.0,
247
+ spent_credit=0.0,
248
+ decayed_credit=current - decayed,
249
+ remaining_credit=decayed,
250
+ reason="automatic_decay",
251
+ capability_scope=capability_scope,
252
+ task_scope=task_scope,
253
+ )
254
+ self.entries.append(entry)
255
+ self._agent_balances[key] = decayed
256
+
257
+ # ------------------------------------------------------------------
258
+ # Anti-gaming helpers
259
+ # ------------------------------------------------------------------
260
+
261
+ def detect_collusion(self, agents: List[str], window: int = 100) -> List[Dict]:
262
+ """Detect suspicious credit-earning patterns across agents."""
263
+ suspicious = []
264
+ recent = self.entries[-window:]
265
+ for entry in recent:
266
+ if entry.reason.startswith("TRANSFER_BLOCKED"):
267
+ # Parse transfer attempt
268
+ parts = entry.reason.split()
269
+ to_agent = parts[-1]
270
+ if to_agent in agents:
271
+ suspicious.append({
272
+ "from": entry.agent_id,
273
+ "to": to_agent,
274
+ "timestamp": entry.timestamp,
275
+ "type": "transfer_attempt",
276
+ })
277
+ return suspicious
278
+
279
+ def audit(self, agent_id: Optional[str] = None) -> Dict:
280
+ """Produce auditable summary."""
281
+ entries = self.entries
282
+ if agent_id:
283
+ entries = [e for e in entries if e.agent_id == agent_id]
284
+
285
+ total_earned = sum(e.earned_credit for e in entries)
286
+ total_spent = sum(e.spent_credit for e in entries)
287
+ total_decayed = sum(e.decayed_credit for e in entries)
288
+
289
+ return {
290
+ "agent_id": agent_id or "all",
291
+ "total_entries": len(entries),
292
+ "total_earned": total_earned,
293
+ "total_spent": total_spent,
294
+ "total_decayed": total_decayed,
295
+ "net_balance": total_earned - total_spent - total_decayed,
296
+ "transfer_attempts": sum(1 for e in entries if e.reason.startswith("TRANSFER_BLOCKED")),
297
+ }
298
+
299
+ # ------------------------------------------------------------------
300
+ # Persistence
301
+ # ------------------------------------------------------------------
302
+
303
+ def save(self, path: Optional[str] = None):
304
+ target = path or self.ledger_path
305
+ if target is None:
306
+ return
307
+ Path(target).parent.mkdir(parents=True, exist_ok=True)
308
+ with open(target, "w") as f:
309
+ for entry in self.entries:
310
+ f.write(json.dumps(asdict(entry), default=str) + "\n")
311
+
312
+ def load(self, path: Optional[str] = None):
313
+ target = path or self.ledger_path
314
+ if target is None or not Path(target).exists():
315
+ return
316
+ self.entries = []
317
+ with open(target, "r") as f:
318
+ for line in f:
319
+ d = json.loads(line)
320
+ self.entries.append(LedgerEntry(**d))
321
+
322
+ # ------------------------------------------------------------------
323
+ # Internal
324
+ # ------------------------------------------------------------------
325
+
326
+ def _invalidate_cache(self, agent_id: str, capability_scope: str, task_scope: str):
327
+ key = (agent_id, capability_scope, task_scope)
328
+ # Recompute from entries
329
+ relevant = [
330
+ e for e in self.entries
331
+ if e.agent_id == agent_id
332
+ and e.capability_scope == capability_scope
333
+ and e.task_scope == task_scope
334
+ ]
335
+ bal = 0.0
336
+ for e in relevant:
337
+ bal += e.earned_credit - e.spent_credit - e.decayed_credit
338
+ self._agent_balances[key] = max(0.0, bal)
339
+
340
+ def _trim_history(self):
341
+ if len(self.entries) > self.max_history:
342
+ self.entries = self.entries[-self.max_history:]