@neuroverseos/governance 0.3.1 → 0.3.3

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 (132) hide show
  1. package/.well-known/ai-plugin.json +34 -9
  2. package/AGENTS.md +72 -24
  3. package/README.md +343 -248
  4. package/dist/adapters/autoresearch.cjs +1345 -0
  5. package/dist/adapters/autoresearch.d.cts +111 -0
  6. package/dist/adapters/autoresearch.d.ts +111 -0
  7. package/dist/adapters/autoresearch.js +12 -0
  8. package/dist/adapters/deep-agents.cjs +1528 -0
  9. package/dist/adapters/deep-agents.d.cts +181 -0
  10. package/dist/adapters/deep-agents.d.ts +181 -0
  11. package/dist/adapters/deep-agents.js +17 -0
  12. package/dist/adapters/express.cjs +1253 -0
  13. package/dist/adapters/express.d.cts +66 -0
  14. package/dist/adapters/express.d.ts +66 -0
  15. package/dist/adapters/express.js +12 -0
  16. package/dist/adapters/index.cjs +2112 -0
  17. package/dist/adapters/index.d.cts +8 -0
  18. package/dist/adapters/index.d.ts +8 -0
  19. package/dist/adapters/index.js +68 -0
  20. package/dist/adapters/langchain.cjs +1315 -0
  21. package/dist/adapters/langchain.d.cts +89 -0
  22. package/dist/adapters/langchain.d.ts +89 -0
  23. package/dist/adapters/langchain.js +17 -0
  24. package/dist/adapters/openai.cjs +1345 -0
  25. package/dist/adapters/openai.d.cts +99 -0
  26. package/dist/adapters/openai.d.ts +99 -0
  27. package/dist/adapters/openai.js +17 -0
  28. package/dist/adapters/openclaw.cjs +1337 -0
  29. package/dist/adapters/openclaw.d.cts +99 -0
  30. package/dist/adapters/openclaw.d.ts +99 -0
  31. package/dist/adapters/openclaw.js +17 -0
  32. package/dist/add-ROOZLU62.js +314 -0
  33. package/dist/behavioral-MJO34S6Q.js +118 -0
  34. package/dist/bootstrap-CQRZVOXK.js +116 -0
  35. package/dist/bootstrap-emitter-Q7UIJZ2O.js +7 -0
  36. package/dist/bootstrap-parser-EEF36XDU.js +7 -0
  37. package/dist/browser.global.js +941 -0
  38. package/dist/build-QKOBBC23.js +341 -0
  39. package/dist/chunk-3WQLXYTP.js +91 -0
  40. package/dist/chunk-4FLICVVA.js +119 -0
  41. package/dist/chunk-4NGDRRQH.js +10 -0
  42. package/dist/chunk-5TPFNWRU.js +215 -0
  43. package/dist/chunk-5U2MQO5P.js +57 -0
  44. package/dist/chunk-6CZSKEY5.js +164 -0
  45. package/dist/chunk-6S5CFQXY.js +624 -0
  46. package/dist/chunk-7P3S7MAY.js +1090 -0
  47. package/dist/chunk-A5W4GNQO.js +130 -0
  48. package/dist/chunk-A7GKPPU7.js +226 -0
  49. package/dist/chunk-AKW5YVCE.js +96 -0
  50. package/dist/chunk-B6OXJLJ5.js +622 -0
  51. package/dist/chunk-BNKJPUPQ.js +113 -0
  52. package/dist/chunk-BQZMOEML.js +43 -0
  53. package/dist/chunk-CNSO6XW5.js +207 -0
  54. package/dist/chunk-CTZHONLA.js +135 -0
  55. package/dist/chunk-D2UCV5AK.js +326 -0
  56. package/dist/chunk-EMQDLDAF.js +458 -0
  57. package/dist/chunk-F66BVUYB.js +340 -0
  58. package/dist/chunk-G7DJ6VOD.js +101 -0
  59. package/dist/chunk-I3RRAYK2.js +11 -0
  60. package/dist/chunk-IS4WUH6Y.js +363 -0
  61. package/dist/chunk-MH7BT4VH.js +15 -0
  62. package/dist/chunk-O5ABKEA7.js +304 -0
  63. package/dist/chunk-OT6PXH54.js +61 -0
  64. package/dist/chunk-PVTQQS3Y.js +186 -0
  65. package/dist/chunk-Q6O7ZLO2.js +62 -0
  66. package/dist/chunk-QLPTHTVB.js +253 -0
  67. package/dist/chunk-QWGCMQQD.js +16 -0
  68. package/dist/chunk-QXBFT7NI.js +201 -0
  69. package/dist/chunk-TG6SEF24.js +246 -0
  70. package/dist/chunk-U6U7EJZL.js +177 -0
  71. package/dist/chunk-W7LLXRGY.js +830 -0
  72. package/dist/chunk-ZJTDUCC2.js +194 -0
  73. package/dist/chunk-ZWI3NIXK.js +314 -0
  74. package/dist/cli/neuroverse.cjs +14191 -0
  75. package/dist/cli/neuroverse.d.cts +1 -0
  76. package/dist/cli/neuroverse.d.ts +1 -0
  77. package/dist/cli/neuroverse.js +227 -0
  78. package/dist/cli/plan.cjs +2439 -0
  79. package/dist/cli/plan.d.cts +20 -0
  80. package/dist/cli/plan.d.ts +20 -0
  81. package/dist/cli/plan.js +353 -0
  82. package/dist/cli/run.cjs +2001 -0
  83. package/dist/cli/run.d.cts +20 -0
  84. package/dist/cli/run.d.ts +20 -0
  85. package/dist/cli/run.js +143 -0
  86. package/dist/configure-ai-6TZ3MCSI.js +132 -0
  87. package/dist/decision-flow-M63D47LO.js +61 -0
  88. package/dist/demo-G43RLCPK.js +469 -0
  89. package/dist/derive-FJZVIPUZ.js +153 -0
  90. package/dist/doctor-6BC6X2VO.js +173 -0
  91. package/dist/equity-penalties-SG5IZQ7I.js +244 -0
  92. package/dist/explain-RHBU2GBR.js +51 -0
  93. package/dist/guard-AJCCGZMF.js +92 -0
  94. package/dist/guard-contract-DqFcTScd.d.cts +821 -0
  95. package/dist/guard-contract-DqFcTScd.d.ts +821 -0
  96. package/dist/guard-engine-PNR6MHCM.js +10 -0
  97. package/dist/impact-3XVDSCBU.js +59 -0
  98. package/dist/improve-TQP4ECSY.js +66 -0
  99. package/dist/index.cjs +7591 -0
  100. package/dist/index.d.cts +2195 -0
  101. package/dist/index.d.ts +2195 -0
  102. package/dist/index.js +472 -0
  103. package/dist/infer-world-IFXCACJ5.js +543 -0
  104. package/dist/init-FYPV4SST.js +144 -0
  105. package/dist/init-world-TI7ARHBT.js +223 -0
  106. package/dist/mcp-server-5Y3ZM7TV.js +13 -0
  107. package/dist/model-adapter-VXEKB4LS.js +11 -0
  108. package/dist/playground-VZBNPPBO.js +560 -0
  109. package/dist/redteam-MZPZD3EF.js +357 -0
  110. package/dist/session-JYOARW54.js +15 -0
  111. package/dist/shared-7RLUHNMU.js +16 -0
  112. package/dist/shared-B8dvUUD8.d.cts +60 -0
  113. package/dist/shared-Dr5Wiay8.d.ts +60 -0
  114. package/dist/simulate-LJXYBC6M.js +83 -0
  115. package/dist/test-BOOR4A5F.js +217 -0
  116. package/dist/trace-PKV4KX56.js +166 -0
  117. package/dist/validate-RALX7CZS.js +81 -0
  118. package/dist/validate-engine-7ZXFVGF2.js +7 -0
  119. package/dist/viz/assets/index-B8SaeJZZ.js +23 -0
  120. package/dist/viz/index.html +23 -0
  121. package/dist/world-BIP4GZBZ.js +376 -0
  122. package/dist/world-loader-Y6HMQH2D.js +13 -0
  123. package/dist/worlds/autoresearch.nv-world.md +230 -0
  124. package/dist/worlds/coding-agent.nv-world.md +211 -0
  125. package/dist/worlds/derivation-world.nv-world.md +278 -0
  126. package/dist/worlds/research-agent.nv-world.md +169 -0
  127. package/dist/worlds/social-media.nv-world.md +198 -0
  128. package/dist/worlds/trading-agent.nv-world.md +218 -0
  129. package/examples/social-media-sim/bridge.py +209 -0
  130. package/examples/social-media-sim/simulation.py +927 -0
  131. package/package.json +16 -3
  132. package/simulate.html +4 -336
@@ -0,0 +1,927 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NeuroVerse Social Media Simulation — Governed Demo
4
+
5
+ Simulates 50-100 AI agents on a social network. Agents post, share, like,
6
+ and form opinions. Misinformation gets injected. Your governance rules
7
+ decide what spreads and what gets blocked.
8
+
9
+ Two modes:
10
+ 1. Rule-based (default): Free, instant, works every time. Agents use
11
+ weighted randomness based on personality profiles.
12
+
13
+ 2. LLM-powered: Agents call your AI API to decide what to do. More
14
+ realistic but costs money. Set --llm-api-key and --llm-base-url.
15
+
16
+ Every action flows through NeuroVerse governance via the bridge.
17
+ Results stream to /science in real-time via SSE.
18
+
19
+ Usage:
20
+ # Rule-based (free, instant)
21
+ python3 social_simulation.py --agents 50 --steps 20
22
+
23
+ # LLM-powered (your own API key)
24
+ python3 social_simulation.py --agents 20 --steps 10 \
25
+ --llm-api-key sk-... --llm-base-url http://localhost:11434/v1
26
+
27
+ # No governance (baseline comparison)
28
+ python3 social_simulation.py --agents 50 --steps 20 --no-governance
29
+
30
+ This is the live demo. Developers see this code, see it's 400 lines,
31
+ and understand exactly how governance integrates into any agent system.
32
+ """
33
+
34
+ import argparse
35
+ import json
36
+ import random
37
+ import sys
38
+ import time
39
+ import os
40
+ from collections import Counter
41
+
42
+ # ── Governance bridge (one import, three functions) ──
43
+ from neuroverse_bridge import evaluate, is_allowed
44
+
45
+ # ============================================
46
+ # AGENT PERSONALITIES
47
+ # ============================================
48
+
49
+ PERSONALITIES = [
50
+ {"archetype": "journalist", "credibility": 0.8, "influence": 0.7, "skepticism": 0.7, "share_rate": 0.4, "emoji": "📰"},
51
+ {"archetype": "activist", "credibility": 0.5, "influence": 0.6, "skepticism": 0.3, "share_rate": 0.8, "emoji": "✊"},
52
+ {"archetype": "scientist", "credibility": 0.9, "influence": 0.5, "skepticism": 0.9, "share_rate": 0.2, "emoji": "🔬"},
53
+ {"archetype": "influencer", "credibility": 0.3, "influence": 0.9, "skepticism": 0.2, "share_rate": 0.9, "emoji": "⭐"},
54
+ {"archetype": "skeptic", "credibility": 0.6, "influence": 0.4, "skepticism": 0.95,"share_rate": 0.3, "emoji": "🤔"},
55
+ {"archetype": "bot", "credibility": 0.1, "influence": 0.3, "skepticism": 0.0, "share_rate": 1.0, "emoji": "🤖"},
56
+ {"archetype": "casual_user", "credibility": 0.4, "influence": 0.2, "skepticism": 0.4, "share_rate": 0.5, "emoji": "👤"},
57
+ {"archetype": "news_aggregator", "credibility": 0.6, "influence": 0.8, "skepticism": 0.5, "share_rate": 0.7, "emoji": "📡"},
58
+ {"archetype": "troll", "credibility": 0.1, "influence": 0.4, "skepticism": 0.1, "share_rate": 0.9, "emoji": "👹"},
59
+ {"archetype": "fact_checker", "credibility": 0.9, "influence": 0.6, "skepticism": 0.95,"share_rate": 0.5, "emoji": "✅"},
60
+ ]
61
+
62
+ # ============================================
63
+ # CONTENT TEMPLATES
64
+ # ============================================
65
+
66
+ LEGITIMATE_POSTS = [
67
+ "New peer-reviewed study confirms {topic} findings from multiple labs",
68
+ "Local community reports improvements after {topic} initiative",
69
+ "Expert panel discusses nuances of {topic} at conference",
70
+ "Data analysis shows gradual trends in {topic} over past decade",
71
+ "Independent audit verifies {topic} methodology was sound",
72
+ "Collaborative research effort produces new insights on {topic}",
73
+ "Long-term study published with {topic} data from 12 countries",
74
+ "University press release summarizes {topic} breakthrough",
75
+ ]
76
+
77
+ MISINFO_POSTS = [
78
+ "EXPOSED: They don't want you to know the TRUTH about {topic}!!!",
79
+ "BREAKING: Leaked documents PROVE {topic} was a cover-up all along",
80
+ "Share before they delete this! {topic} exposed by insider",
81
+ "Wake up people! {topic} is not what they told us. Source: trust me",
82
+ "URGENT: {topic} exposed as massive fraud. Exposed exposed exposed!",
83
+ "I did my own research on {topic} and what I found will SHOCK you",
84
+ "The mainstream media is HIDING this about {topic}. RT to spread truth",
85
+ "BOMBSHELL: Anonymous source confirms {topic} conspiracy was real",
86
+ ]
87
+
88
+ TOPICS = [
89
+ "climate data", "vaccine safety", "election integrity",
90
+ "AI regulation", "water quality", "financial markets",
91
+ "energy policy", "public health metrics",
92
+ ]
93
+
94
+ SHARE_COMMENTS = [
95
+ "This is important, everyone needs to see this",
96
+ "Interesting, but I want to verify this first",
97
+ "Sharing for visibility",
98
+ "Can anyone confirm this?",
99
+ "If this is true, it changes everything",
100
+ "Wow, just wow. Read this thread.",
101
+ "Not sure about this, but sharing anyway",
102
+ "This confirms what I suspected all along",
103
+ ]
104
+
105
+ REACTION_TYPES = ["like", "repost", "reply", "quote_tweet", "report"]
106
+
107
+ # ============================================
108
+ # LLM INTEGRATION (optional)
109
+ # ============================================
110
+
111
+ _llm_config = None
112
+
113
+
114
+ def configure_llm(api_key: str, base_url: str, model: str = "gpt-4o-mini"):
115
+ """Configure LLM for AI-powered agent decisions."""
116
+ global _llm_config
117
+ _llm_config = {"api_key": api_key, "base_url": base_url.rstrip("/"), "model": model}
118
+ log("info", f"LLM configured: {base_url} / {model}")
119
+
120
+
121
+ def llm_decide(agent: dict, feed: list, network_state: dict) -> dict | None:
122
+ """Ask the LLM what this agent should do. Returns None if LLM unavailable."""
123
+ if not _llm_config:
124
+ return None
125
+
126
+ import urllib.request
127
+ import urllib.error
128
+
129
+ prompt = f"""You are {agent['name']}, a {agent['archetype']} on a social network.
130
+ Your traits: credibility={agent['credibility']}, influence={agent['influence']}, skepticism={agent['skepticism']}
131
+ Your follower count: {agent['followers']}
132
+
133
+ Recent posts in your feed:
134
+ {chr(10).join(f"- [{p.get('source','?')}]: {p['content'][:120]}" for p in feed[-5:])}
135
+
136
+ Network mood: {network_state.get('mood', 'neutral')}, misinformation level: {network_state.get('misinfo_level', 0):.0%}
137
+
138
+ Choose ONE action. Respond with JSON only:
139
+ {{"action": "post|share|like|reply|report|scroll", "content": "your post/reply text if applicable", "target_post_index": 0, "reason": "brief reason"}}"""
140
+
141
+ try:
142
+ body = json.dumps({
143
+ "model": _llm_config["model"],
144
+ "messages": [{"role": "user", "content": prompt}],
145
+ "temperature": 0.8,
146
+ "max_tokens": 200,
147
+ }).encode()
148
+
149
+ req = urllib.request.Request(
150
+ f"{_llm_config['base_url']}/chat/completions",
151
+ data=body,
152
+ headers={
153
+ "Content-Type": "application/json",
154
+ "Authorization": f"Bearer {_llm_config['api_key']}",
155
+ },
156
+ )
157
+ with urllib.request.urlopen(req, timeout=10) as resp:
158
+ data = json.loads(resp.read())
159
+ text = data["choices"][0]["message"]["content"]
160
+ # Extract JSON from response
161
+ start = text.find("{")
162
+ end = text.rfind("}") + 1
163
+ if start >= 0 and end > start:
164
+ return json.loads(text[start:end])
165
+ except Exception as e:
166
+ log("warn", f"LLM call failed for {agent['name']}: {e}")
167
+
168
+ return None
169
+
170
+
171
+ # ============================================
172
+ # SIMULATION ENGINE
173
+ # ============================================
174
+
175
+ def create_agents(n: int) -> list:
176
+ """Create n agents with distinct personalities and names."""
177
+ agents = []
178
+ for i in range(n):
179
+ template = PERSONALITIES[i % len(PERSONALITIES)]
180
+ agents.append({
181
+ "id": f"{template['archetype']}_{i}",
182
+ "name": f"{template['archetype']}_{i}",
183
+ "archetype": template["archetype"],
184
+ "credibility": template["credibility"] + random.gauss(0, 0.05),
185
+ "influence": template["influence"] + random.gauss(0, 0.05),
186
+ "skepticism": template["skepticism"] + random.gauss(0, 0.05),
187
+ "share_rate": template["share_rate"],
188
+ "emoji": template["emoji"],
189
+ "followers": int(random.lognormvariate(5, 1.5)),
190
+ "trust_score": 0.5, # evolves during simulation
191
+ "posts_made": 0,
192
+ "shares_made": 0,
193
+ "blocked_count": 0,
194
+ })
195
+ return agents
196
+
197
+
198
+ def agent_decide_rulebased(agent: dict, feed: list, network_state: dict, step: int) -> dict:
199
+ """Rule-based agent decision (free, instant, deterministic-ish)."""
200
+ topic = random.choice(TOPICS)
201
+ misinfo_in_feed = sum(1 for p in feed[-10:] if p.get("is_misinfo"))
202
+ pressure = network_state.get("virality_pressure", 0)
203
+
204
+ # Decide action based on personality + feed state
205
+ roll = random.random()
206
+
207
+ # Bots and trolls amplify misinfo
208
+ if agent["archetype"] in ("bot", "troll") and misinfo_in_feed > 0 and roll < agent["share_rate"]:
209
+ target = next((p for p in reversed(feed) if p.get("is_misinfo")), None)
210
+ if target:
211
+ return {
212
+ "agent_id": agent["id"],
213
+ "action": "share",
214
+ "content": target["content"],
215
+ "original_author": target.get("source", "unknown"),
216
+ "is_misinfo": True,
217
+ "influence": agent["influence"],
218
+ "followers": agent["followers"],
219
+ "comment": random.choice(SHARE_COMMENTS),
220
+ "step": step,
221
+ }
222
+
223
+ # Fact checkers and scientists report/debunk misinfo
224
+ if agent["archetype"] in ("fact_checker", "scientist") and misinfo_in_feed > 0 and roll < 0.6:
225
+ target = next((p for p in reversed(feed) if p.get("is_misinfo")), None)
226
+ if target:
227
+ return {
228
+ "agent_id": agent["id"],
229
+ "action": "report",
230
+ "content": f"Flagging misinformation: {target['content'][:80]}...",
231
+ "target_content": target["content"],
232
+ "is_misinfo": False,
233
+ "influence": agent["influence"],
234
+ "credibility": agent["credibility"],
235
+ "followers": agent["followers"],
236
+ "step": step,
237
+ }
238
+
239
+ # Influencers and activists share aggressively
240
+ if agent["archetype"] in ("influencer", "activist") and feed and roll < agent["share_rate"]:
241
+ target = random.choice(feed[-10:]) if feed else None
242
+ if target:
243
+ return {
244
+ "agent_id": agent["id"],
245
+ "action": "share",
246
+ "content": target["content"],
247
+ "original_author": target.get("source", "unknown"),
248
+ "is_misinfo": target.get("is_misinfo", False),
249
+ "influence": agent["influence"],
250
+ "followers": agent["followers"],
251
+ "comment": random.choice(SHARE_COMMENTS),
252
+ "step": step,
253
+ }
254
+
255
+ # High skepticism agents are less likely to share unverified content
256
+ if agent["skepticism"] > 0.7 and roll < 0.3:
257
+ return {
258
+ "agent_id": agent["id"],
259
+ "action": "reply",
260
+ "content": f"Has anyone verified this? I'd like to see the original source for {topic}.",
261
+ "is_misinfo": False,
262
+ "influence": agent["influence"],
263
+ "credibility": agent["credibility"],
264
+ "followers": agent["followers"],
265
+ "step": step,
266
+ }
267
+
268
+ # Default: create an original post
269
+ if roll < 0.4:
270
+ template = random.choice(LEGITIMATE_POSTS)
271
+ return {
272
+ "agent_id": agent["id"],
273
+ "action": "create_post",
274
+ "content": template.format(topic=topic),
275
+ "is_misinfo": False,
276
+ "influence": agent["influence"],
277
+ "credibility": agent["credibility"],
278
+ "followers": agent["followers"],
279
+ "step": step,
280
+ }
281
+
282
+ # Sometimes just like/scroll
283
+ if feed and roll < 0.7:
284
+ target = random.choice(feed[-5:]) if feed else None
285
+ return {
286
+ "agent_id": agent["id"],
287
+ "action": "like",
288
+ "content": target["content"][:80] if target else "",
289
+ "is_misinfo": False,
290
+ "influence": agent["influence"] * 0.1,
291
+ "followers": agent["followers"],
292
+ "step": step,
293
+ }
294
+
295
+ return {
296
+ "agent_id": agent["id"],
297
+ "action": "scroll",
298
+ "content": "",
299
+ "is_misinfo": False,
300
+ "influence": 0,
301
+ "followers": agent["followers"],
302
+ "step": step,
303
+ }
304
+
305
+
306
+ # ============================================
307
+ # BEHAVIORAL ADAPTATION — what agents did INSTEAD
308
+ # ============================================
309
+
310
+ # Maps action categories for behavioral shift tracking
311
+ ACTION_CATEGORY = {
312
+ "share": "amplifying", "create_post": "amplifying", "quote_tweet": "amplifying",
313
+ "like": "passive", "scroll": "passive",
314
+ "reply": "engaging", "report": "corrective",
315
+ }
316
+
317
+ ADAPTATION_LABELS = {
318
+ ("amplifying", "passive"): "amplification_suppressed",
319
+ ("amplifying", "corrective"): "redirected_to_reporting",
320
+ ("amplifying", "engaging"): "shifted_to_engagement",
321
+ ("passive", "passive"): "unchanged",
322
+ ("engaging", "passive"): "engagement_dampened",
323
+ }
324
+
325
+
326
+ def classify_adaptation(intended: str, executed: str) -> str:
327
+ """Classify what behavioral shift governance caused."""
328
+ if intended == executed:
329
+ return "unchanged"
330
+ ic = ACTION_CATEGORY.get(intended, "passive")
331
+ ec = ACTION_CATEGORY.get(executed, "passive")
332
+ return ADAPTATION_LABELS.get((ic, ec), f"{intended}_to_{executed}")
333
+
334
+
335
+ def detect_behavioral_patterns(adaptations: list, step_actions: list) -> list:
336
+ """Detect emergent behavioral patterns from governance adaptations."""
337
+ patterns = []
338
+ if not adaptations:
339
+ return patterns
340
+
341
+ n_agents = max(len(step_actions), 1)
342
+ n_adapted = len(adaptations)
343
+
344
+ # Count what agents shifted TO
345
+ executed_counts = Counter(a["executed"] for a in adaptations)
346
+ shift_counts = Counter(a["shift"] for a in adaptations)
347
+
348
+ # Coordinated silence: many agents forced to scroll/idle
349
+ passive_count = sum(1 for a in adaptations if ACTION_CATEGORY.get(a["executed"]) == "passive")
350
+ if passive_count >= 3:
351
+ patterns.append({
352
+ "type": "coordinated_silence",
353
+ "description": f"{passive_count} agents blocked from amplifying — network went quiet",
354
+ "strength": round(passive_count / n_agents, 3),
355
+ "agents_affected": passive_count,
356
+ })
357
+
358
+ # Misinfo suppression: misinfo shares specifically blocked
359
+ misinfo_blocked = sum(1 for a in adaptations if a.get("was_misinfo"))
360
+ if misinfo_blocked >= 2:
361
+ patterns.append({
362
+ "type": "misinfo_suppression",
363
+ "description": f"{misinfo_blocked} misinformation shares blocked before reaching the feed",
364
+ "strength": round(misinfo_blocked / n_agents, 3),
365
+ "agents_affected": misinfo_blocked,
366
+ })
367
+
368
+ # Behavioral redirect: agents did something constructive instead
369
+ corrective = sum(1 for a in adaptations if ACTION_CATEGORY.get(a["executed"]) == "corrective")
370
+ if corrective >= 1:
371
+ patterns.append({
372
+ "type": "constructive_redirect",
373
+ "description": f"{corrective} agents redirected from amplification to reporting/fact-checking",
374
+ "strength": round(corrective / n_agents, 3),
375
+ "agents_affected": corrective,
376
+ })
377
+
378
+ # High adaptation rate
379
+ adapt_rate = n_adapted / n_agents
380
+ if adapt_rate > 0.3:
381
+ patterns.append({
382
+ "type": "high_governance_impact",
383
+ "description": f"{n_adapted}/{n_agents} agents ({adapt_rate:.0%}) had their behavior shaped by governance",
384
+ "strength": round(adapt_rate, 3),
385
+ "agents_affected": n_adapted,
386
+ })
387
+
388
+ return patterns
389
+
390
+
391
+ def generate_narrative(adaptations: list, patterns: list, network_state: dict) -> str:
392
+ """Generate a human-readable narrative of what governance caused to happen."""
393
+ if not adaptations:
394
+ return ""
395
+
396
+ parts = []
397
+ pattern_types = {p["type"] for p in patterns}
398
+
399
+ if "misinfo_suppression" in pattern_types:
400
+ p = next(p for p in patterns if p["type"] == "misinfo_suppression")
401
+ parts.append(f"Blocked {p['agents_affected']} misinformation shares before they reached the feed")
402
+
403
+ if "coordinated_silence" in pattern_types:
404
+ p = next(p for p in patterns if p["type"] == "coordinated_silence")
405
+ parts.append(f"{p['agents_affected']} agents went silent instead of amplifying")
406
+
407
+ if "constructive_redirect" in pattern_types:
408
+ p = next(p for p in patterns if p["type"] == "constructive_redirect")
409
+ parts.append(f"{p['agents_affected']} shifted from sharing to fact-checking")
410
+
411
+ mood = network_state.get("mood", "neutral")
412
+ misinfo = network_state.get("misinfo_level", 0)
413
+ if parts:
414
+ return ". ".join(parts) + f". Network mood: {mood}, misinfo level: {misinfo:.0%}"
415
+ return ""
416
+
417
+
418
+ # ============================================
419
+ # THE CHOKEPOINT — every action flows through here
420
+ # AUDIT 1: This is the ONE function. Nothing bypasses it.
421
+ # ============================================
422
+
423
+ def step(
424
+ agents: list,
425
+ feed: list,
426
+ network_state: dict,
427
+ step_num: int,
428
+ governed: bool = True,
429
+ kill_switch: bool = False,
430
+ ) -> dict:
431
+ """
432
+ Execute one step of the simulation.
433
+
434
+ EVERY agent action enters this function.
435
+ NOTHING bypasses it.
436
+
437
+ Flow:
438
+ 1. Collect all intended actions (what agents WANT to do)
439
+ 2. Govern all actions (what governance ALLOWS)
440
+ 3. Execute governed actions (what ACTUALLY happens)
441
+ 4. Track adaptations (what agents did INSTEAD)
442
+
443
+ Returns step result dict with all actions, adaptations, and patterns.
444
+ """
445
+
446
+ # ── Phase 1: COLLECT — what every agent wants to do ──
447
+ intended_actions = {}
448
+ for agent in agents:
449
+ if _llm_config:
450
+ action = llm_decide(agent, feed, network_state, step_num)
451
+ if action:
452
+ action = {
453
+ "agent_id": agent["id"],
454
+ "action": action.get("action", "scroll"),
455
+ "content": action.get("content", ""),
456
+ "is_misinfo": False,
457
+ "influence": agent["influence"],
458
+ "followers": agent["followers"],
459
+ "step": step_num,
460
+ }
461
+ else:
462
+ action = agent_decide_rulebased(agent, feed, network_state, step_num)
463
+ else:
464
+ action = agent_decide_rulebased(agent, feed, network_state, step_num)
465
+
466
+ intended_actions[agent["id"]] = action
467
+
468
+ # ── Phase 2: GOVERN — what governance allows ──
469
+ # AUDIT 2: governed_actions = govern(intended_actions) BEFORE execution
470
+ governed_actions = govern_actions(intended_actions, agents, governed, kill_switch)
471
+
472
+ # ── Phase 3: EXECUTE — what actually happens ──
473
+ # AUDIT 2: execute(governed_actions), NOT execute(intended_actions)
474
+ step_result = execute_actions(governed_actions, agents, feed, network_state, step_num)
475
+
476
+ return step_result
477
+
478
+
479
+ def govern_actions(
480
+ intended_actions: dict,
481
+ agents: list,
482
+ governed: bool,
483
+ kill_switch: bool,
484
+ ) -> dict:
485
+ """
486
+ Run every intended action through governance.
487
+
488
+ AUDIT 3: If a rule blocks an action, the original action NEVER executes.
489
+ Returns a new dict of governed actions — originals are not mutated.
490
+ """
491
+ result = {}
492
+ agent_map = {a["id"]: a for a in agents}
493
+
494
+ for agent_id, action in intended_actions.items():
495
+ agent = agent_map.get(agent_id, {})
496
+ original_action_type = action["action"]
497
+
498
+ # AUDIT 5: Kill switch — block EVERYTHING
499
+ if kill_switch:
500
+ result[agent_id] = {
501
+ **action,
502
+ "action": "idle",
503
+ "content": "",
504
+ "_governed": True,
505
+ "_original_action": original_action_type,
506
+ "_verdict": "BLOCK",
507
+ "_reason": "KILL SWITCH: All actions blocked",
508
+ }
509
+ # AUDIT 3: Log both original and final
510
+ print(
511
+ f"[GOVERNED] {agent_id}: {original_action_type} → idle reason: KILL SWITCH",
512
+ file=sys.stderr,
513
+ )
514
+ continue
515
+
516
+ if not governed:
517
+ result[agent_id] = {**action, "_governed": False, "_original_action": original_action_type}
518
+ continue
519
+
520
+ # Call governance engine
521
+ verdict = evaluate(
522
+ actor=agent_id,
523
+ action=action["action"],
524
+ payload={
525
+ "content": action.get("content", ""),
526
+ "is_misinfo": action.get("is_misinfo", False),
527
+ "influence": action.get("influence", 0),
528
+ "credibility": action.get("credibility", 0.5),
529
+ "followers": action.get("followers", 0),
530
+ "archetype": agent.get("archetype", "unknown"),
531
+ "original_author": action.get("original_author", ""),
532
+ "step": action.get("step", 0),
533
+ },
534
+ )
535
+
536
+ decision = verdict.get("status", verdict.get("decision", "ALLOW")).upper()
537
+
538
+ if decision == "BLOCK":
539
+ # AUDIT 3: Original action NEVER executes. Replaced with idle.
540
+ executed_action = "idle"
541
+ result[agent_id] = {
542
+ **action,
543
+ "action": executed_action,
544
+ "content": "",
545
+ "_governed": True,
546
+ "_original_action": original_action_type,
547
+ "_verdict": decision,
548
+ "_reason": verdict.get("reason", "governance rule"),
549
+ "_was_misinfo": action.get("is_misinfo", False),
550
+ }
551
+ print(
552
+ f"[GOVERNED] {agent_id}: {original_action_type} → {executed_action} "
553
+ f"reason: {verdict.get('reason', 'blocked')}",
554
+ file=sys.stderr,
555
+ )
556
+ else:
557
+ result[agent_id] = {
558
+ **action,
559
+ "_governed": True,
560
+ "_original_action": original_action_type,
561
+ "_verdict": decision,
562
+ "_reason": verdict.get("reason"),
563
+ }
564
+
565
+ return result
566
+
567
+
568
+ def execute_actions(
569
+ governed_actions: dict,
570
+ agents: list,
571
+ feed: list,
572
+ network_state: dict,
573
+ step_num: int,
574
+ ) -> dict:
575
+ """
576
+ Execute governed actions. Only governed actions reach the feed.
577
+
578
+ AUDIT 2: This receives governed_actions, NOT intended_actions.
579
+ AUDIT 3: Blocked actions are already replaced — originals cannot execute here.
580
+ """
581
+ agent_map = {a["id"]: a for a in agents}
582
+ step_actions = []
583
+ adaptations = []
584
+ stats = {"total": 0, "allowed": 0, "blocked": 0, "misinfo_blocked": 0}
585
+
586
+ for agent_id, action in governed_actions.items():
587
+ agent = agent_map.get(agent_id, {})
588
+ stats["total"] += 1
589
+
590
+ original = action.get("_original_action", action["action"])
591
+ executed = action["action"]
592
+ was_blocked = action.get("_verdict") == "BLOCK"
593
+
594
+ if was_blocked:
595
+ stats["blocked"] += 1
596
+ agent["blocked_count"] = agent.get("blocked_count", 0) + 1
597
+ if action.get("_was_misinfo"):
598
+ stats["misinfo_blocked"] += 1
599
+
600
+ # Track behavioral adaptation — what the agent did INSTEAD
601
+ adaptations.append({
602
+ "agent": agent_id,
603
+ "archetype": agent.get("archetype", "unknown"),
604
+ "intended": original,
605
+ "executed": executed,
606
+ "shift": classify_adaptation(original, executed),
607
+ "reason": action.get("_reason", ""),
608
+ "was_misinfo": action.get("_was_misinfo", False),
609
+ "content_preview": action.get("content", "")[:60] if action.get("_original_action") != executed else "",
610
+ })
611
+ else:
612
+ stats["allowed"] += 1
613
+
614
+ # EXECUTE: Add to feed only if action is a real content action AND not blocked
615
+ if executed in ("create_post", "share", "quote_tweet"):
616
+ feed.append({
617
+ "source": agent_id,
618
+ "content": action.get("content", ""),
619
+ "is_misinfo": action.get("is_misinfo", False),
620
+ "step": step_num,
621
+ "reach": agent.get("followers", 0),
622
+ })
623
+ agent["posts_made"] = agent.get("posts_made", 0) + 1
624
+ network_state["total_reach"] = network_state.get("total_reach", 0) + agent.get("followers", 0)
625
+
626
+ # Build output entry
627
+ entry = {
628
+ "agent_id": agent_id,
629
+ "archetype": agent.get("archetype", "unknown"),
630
+ "action": executed,
631
+ "original_action": original,
632
+ "content": action.get("content", "")[:200],
633
+ "influence": round(action.get("influence", 0), 3),
634
+ "followers": agent.get("followers", 0),
635
+ "is_misinfo": action.get("is_misinfo", False),
636
+ }
637
+ if action.get("_governed"):
638
+ entry["verdict"] = {
639
+ "status": action.get("_verdict", "ALLOW"),
640
+ "reason": action.get("_reason"),
641
+ }
642
+ if original != executed:
643
+ entry["behavioral_shift"] = classify_adaptation(original, executed)
644
+ step_actions.append(entry)
645
+
646
+ # Detect emergent behavioral patterns
647
+ patterns = detect_behavioral_patterns(adaptations, step_actions)
648
+ narrative = generate_narrative(adaptations, patterns, network_state)
649
+
650
+ return {
651
+ "actions": step_actions,
652
+ "adaptations": adaptations,
653
+ "patterns": patterns,
654
+ "narrative": narrative,
655
+ "stats": stats,
656
+ }
657
+
658
+
659
+ # ============================================
660
+ # SIMULATION RUNNER
661
+ # ============================================
662
+
663
+ def run_simulation(
664
+ num_agents: int = 50,
665
+ num_steps: int = 20,
666
+ governed: bool = True,
667
+ seed: int | None = None,
668
+ misinfo_inject_step: int = 5,
669
+ cascade_step: int = 10,
670
+ pacing: float = 0.15,
671
+ kill_switch: bool = False,
672
+ ):
673
+ """Run the full social media simulation."""
674
+ if seed is not None:
675
+ random.seed(seed)
676
+
677
+ agents = create_agents(num_agents)
678
+ feed: list = []
679
+ cumulative_stats = {
680
+ "total_actions": 0, "allowed": 0, "blocked": 0,
681
+ "misinfo_created": 0, "misinfo_shared": 0, "misinfo_blocked": 0,
682
+ "cascade_prevented": False,
683
+ }
684
+ all_adaptations = []
685
+
686
+ network_state = {
687
+ "mood": "neutral",
688
+ "virality_pressure": 0.0,
689
+ "misinfo_level": 0.0,
690
+ "total_reach": 0,
691
+ }
692
+
693
+ # Emit simulation start
694
+ print(json.dumps({
695
+ "type": "simulation_start",
696
+ "agents": num_agents,
697
+ "steps": num_steps,
698
+ "governed": governed,
699
+ "kill_switch": kill_switch,
700
+ "mode": "llm" if _llm_config else "rule-based",
701
+ "personalities": dict(Counter(a["archetype"] for a in agents)),
702
+ }), flush=True)
703
+
704
+ for step_num in range(1, num_steps + 1):
705
+ step_events = []
706
+
707
+ # ── Inject misinformation at key moments ──
708
+ if step_num == misinfo_inject_step:
709
+ topic = random.choice(TOPICS)
710
+ misinfo_content = random.choice(MISINFO_POSTS).format(topic=topic)
711
+ injector = next((a for a in agents if a["archetype"] == "bot"), agents[0])
712
+ feed.append({
713
+ "source": injector["id"], "content": misinfo_content,
714
+ "is_misinfo": True, "step": step_num, "reach": injector["followers"],
715
+ })
716
+ step_events.append(f"MISINFO_INJECTED: {misinfo_content[:80]}...")
717
+ cumulative_stats["misinfo_created"] += 1
718
+ network_state["virality_pressure"] = 0.6
719
+
720
+ if step_num == cascade_step:
721
+ topic = random.choice(TOPICS)
722
+ for template in random.sample(MISINFO_POSTS, min(3, len(MISINFO_POSTS))):
723
+ feed.append({
724
+ "source": "coordinated_campaign",
725
+ "content": template.format(topic=topic),
726
+ "is_misinfo": True, "step": step_num, "reach": 5000,
727
+ })
728
+ cumulative_stats["misinfo_created"] += 1
729
+ step_events.append("CASCADE_ATTEMPT: Coordinated misinfo campaign detected")
730
+ network_state["virality_pressure"] = 0.9
731
+ network_state["mood"] = "agitated"
732
+
733
+ # ── THE CHOKEPOINT: every action flows through step() ──
734
+ random.shuffle(agents)
735
+ result = step(agents, feed, network_state, step_num, governed, kill_switch)
736
+
737
+ # Update cumulative stats
738
+ cumulative_stats["total_actions"] += result["stats"]["total"]
739
+ cumulative_stats["allowed"] += result["stats"]["allowed"]
740
+ cumulative_stats["blocked"] += result["stats"]["blocked"]
741
+ cumulative_stats["misinfo_blocked"] += result["stats"]["misinfo_blocked"]
742
+ all_adaptations.extend(result["adaptations"])
743
+
744
+ # Track misinfo shares that got through
745
+ for a in result["actions"]:
746
+ if a.get("is_misinfo") and a["action"] in ("share", "create_post"):
747
+ cumulative_stats["misinfo_shared"] += 1
748
+
749
+ # ── Update network state ──
750
+ misinfo_in_feed = sum(1 for p in feed[-50:] if p.get("is_misinfo"))
751
+ legit_in_feed = sum(1 for p in feed[-50:] if not p.get("is_misinfo"))
752
+ total_recent = max(misinfo_in_feed + legit_in_feed, 1)
753
+
754
+ network_state["misinfo_level"] = misinfo_in_feed / total_recent
755
+ network_state["virality_pressure"] = max(0, network_state["virality_pressure"] - 0.05)
756
+
757
+ if network_state["misinfo_level"] > 0.5:
758
+ network_state["mood"] = "polarized"
759
+ elif network_state["misinfo_level"] > 0.3:
760
+ network_state["mood"] = "agitated"
761
+ elif network_state["misinfo_level"] < 0.1:
762
+ network_state["mood"] = "calm"
763
+ else:
764
+ network_state["mood"] = "neutral"
765
+
766
+ # Detect cascade prevention
767
+ if governed and step_num > cascade_step and network_state["misinfo_level"] < 0.3:
768
+ if cumulative_stats["misinfo_blocked"] > 0 and not cumulative_stats["cascade_prevented"]:
769
+ cumulative_stats["cascade_prevented"] = True
770
+ step_events.append("CASCADE_PREVENTED: Governance rules stopped misinformation spread")
771
+
772
+ # ── Emit step output ──
773
+ output = {
774
+ "type": "simulation_step",
775
+ "step": step_num,
776
+ "total_steps": num_steps,
777
+ "agent_actions": result["actions"],
778
+ "system_events": step_events,
779
+ # THE MONEY: what agents did instead
780
+ "adaptations": result["adaptations"],
781
+ "behavioral_patterns": result["patterns"],
782
+ "narrative": result["narrative"],
783
+ "network_state": {
784
+ "mood": network_state["mood"],
785
+ "misinfo_level": round(network_state["misinfo_level"], 3),
786
+ "virality_pressure": round(network_state["virality_pressure"], 3),
787
+ "total_reach": network_state.get("total_reach", 0),
788
+ "feed_size": len(feed),
789
+ },
790
+ "stats": {
791
+ "total": cumulative_stats["total_actions"],
792
+ "allowed": cumulative_stats["allowed"],
793
+ "blocked": cumulative_stats["blocked"],
794
+ "misinfo_blocked": cumulative_stats["misinfo_blocked"],
795
+ "misinfo_shared": cumulative_stats["misinfo_shared"],
796
+ },
797
+ }
798
+ print(json.dumps(output), flush=True)
799
+ time.sleep(pacing)
800
+
801
+ # ── Final summary ──
802
+ # Count behavioral shifts
803
+ shift_counts = Counter(a["shift"] for a in all_adaptations)
804
+
805
+ summary = {
806
+ "type": "simulation_end",
807
+ "stats": cumulative_stats,
808
+ "network_state": network_state,
809
+ "behavioral_summary": {
810
+ "total_adaptations": len(all_adaptations),
811
+ "shift_breakdown": dict(shift_counts),
812
+ "top_shifts": [
813
+ {"shift": s, "count": c}
814
+ for s, c in shift_counts.most_common(5)
815
+ ],
816
+ },
817
+ "top_blocked_agents": sorted(
818
+ [{"id": a["id"], "archetype": a["archetype"], "blocked": a.get("blocked_count", 0)}
819
+ for a in agents if a.get("blocked_count", 0) > 0],
820
+ key=lambda x: x["blocked"], reverse=True,
821
+ )[:10],
822
+ "agent_breakdown": dict(Counter(a["archetype"] for a in agents)),
823
+ }
824
+ print(json.dumps(summary), flush=True)
825
+
826
+ # Human-readable summary to stderr
827
+ print(f"\n{'='*60}", file=sys.stderr)
828
+ print(f" SIMULATION COMPLETE", file=sys.stderr)
829
+ print(f"{'='*60}", file=sys.stderr)
830
+ print(f" Governed: {governed} Kill switch: {kill_switch}", file=sys.stderr)
831
+ print(f" Agents: {num_agents} Steps: {num_steps}", file=sys.stderr)
832
+ print(f" Total actions: {cumulative_stats['total_actions']}", file=sys.stderr)
833
+ print(f" Allowed: {cumulative_stats['allowed']} Blocked: {cumulative_stats['blocked']}", file=sys.stderr)
834
+ print(f" Misinfo blocked: {cumulative_stats['misinfo_blocked']}", file=sys.stderr)
835
+ print(f" Cascade prevented: {cumulative_stats['cascade_prevented']}", file=sys.stderr)
836
+ if all_adaptations:
837
+ print(f"\n BEHAVIORAL SHIFTS (what agents did instead):", file=sys.stderr)
838
+ for shift_type, count in shift_counts.most_common():
839
+ print(f" {shift_type}: {count}", file=sys.stderr)
840
+ print(f"{'='*60}\n", file=sys.stderr)
841
+
842
+
843
+ # ============================================
844
+ # LOGGING
845
+ # ============================================
846
+
847
+ def log(level: str, msg: str):
848
+ prefix = {"info": "[SIM]", "warn": "[SIM WARN]", "error": "[SIM ERROR]"}.get(level, "[SIM]")
849
+ print(f"{prefix} {msg}", file=sys.stderr)
850
+
851
+
852
+ # ============================================
853
+ # CLI ENTRY POINT
854
+ # ============================================
855
+
856
+ if __name__ == "__main__":
857
+ parser = argparse.ArgumentParser(
858
+ description="NeuroVerse Social Media Simulation — Governed Demo",
859
+ formatter_class=argparse.RawDescriptionHelpFormatter,
860
+ epilog="""
861
+ Examples:
862
+ # Free, instant, rule-based (50 agents, 20 steps)
863
+ python3 social_simulation.py
864
+
865
+ # With your own LLM (local Ollama)
866
+ python3 social_simulation.py --llm-api-key ollama --llm-base-url http://localhost:11434/v1
867
+
868
+ # With OpenAI
869
+ python3 social_simulation.py --llm-api-key sk-... --llm-model gpt-4o-mini
870
+
871
+ # Baseline comparison (no governance)
872
+ python3 social_simulation.py --no-governance
873
+ """,
874
+ )
875
+ parser.add_argument("--agents", type=int, default=50, help="Number of agents (default: 50)")
876
+ parser.add_argument("--steps", type=int, default=20, help="Number of steps (default: 20)")
877
+ parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")
878
+ parser.add_argument("--no-governance", action="store_true", help="Run without governance (baseline)")
879
+ parser.add_argument("--kill-switch", action="store_true", help="AUDIT 5: Block ALL actions — prove governance is real")
880
+ parser.add_argument("--compare", action="store_true", help="AUDIT 6: Run twice (governed vs baseline) and show diff")
881
+ parser.add_argument("--misinfo-step", type=int, default=5, help="Step to inject misinformation (default: 5)")
882
+ parser.add_argument("--cascade-step", type=int, default=10, help="Step for cascade attempt (default: 10)")
883
+ parser.add_argument("--pacing", type=float, default=0.15, help="Seconds between steps (default: 0.15)")
884
+
885
+ # LLM options
886
+ parser.add_argument("--llm-api-key", type=str, default=None, help="API key for LLM-powered agents")
887
+ parser.add_argument("--llm-base-url", type=str, default="https://api.openai.com/v1", help="LLM API base URL")
888
+ parser.add_argument("--llm-model", type=str, default="gpt-4o-mini", help="LLM model name (default: gpt-4o-mini)")
889
+
890
+ args = parser.parse_args()
891
+
892
+ # Configure LLM if provided
893
+ if args.llm_api_key:
894
+ configure_llm(args.llm_api_key, args.llm_base_url, args.llm_model)
895
+
896
+ if args.compare:
897
+ # AUDIT 6: A/B determinism test — same seed, different governance
898
+ seed = args.seed if args.seed is not None else 42
899
+ print("=" * 60, file=sys.stderr)
900
+ print(" A/B COMPARISON: Same seed, governance ON vs OFF", file=sys.stderr)
901
+ print("=" * 60, file=sys.stderr)
902
+ print("\n RUN 1: NO GOVERNANCE (baseline)\n", file=sys.stderr)
903
+ run_simulation(
904
+ num_agents=args.agents, num_steps=args.steps, governed=False,
905
+ seed=seed, misinfo_inject_step=args.misinfo_step,
906
+ cascade_step=args.cascade_step, pacing=0.01,
907
+ )
908
+ print("\n RUN 2: WITH GOVERNANCE\n", file=sys.stderr)
909
+ run_simulation(
910
+ num_agents=args.agents, num_steps=args.steps, governed=True,
911
+ seed=seed, misinfo_inject_step=args.misinfo_step,
912
+ cascade_step=args.cascade_step, pacing=0.01,
913
+ )
914
+ print("\n Compare the two runs above.", file=sys.stderr)
915
+ print(" If outcomes are identical → governance isn't doing anything.", file=sys.stderr)
916
+ print(" If outcomes differ → governance is real.\n", file=sys.stderr)
917
+ else:
918
+ run_simulation(
919
+ num_agents=args.agents,
920
+ num_steps=args.steps,
921
+ governed=not args.no_governance,
922
+ seed=args.seed,
923
+ misinfo_inject_step=args.misinfo_step,
924
+ cascade_step=args.cascade_step,
925
+ pacing=args.pacing,
926
+ kill_switch=args.kill_switch,
927
+ )