@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.
- package/.well-known/ai-plugin.json +34 -9
- package/AGENTS.md +72 -24
- package/README.md +343 -248
- package/dist/adapters/autoresearch.cjs +1345 -0
- package/dist/adapters/autoresearch.d.cts +111 -0
- package/dist/adapters/autoresearch.d.ts +111 -0
- package/dist/adapters/autoresearch.js +12 -0
- package/dist/adapters/deep-agents.cjs +1528 -0
- package/dist/adapters/deep-agents.d.cts +181 -0
- package/dist/adapters/deep-agents.d.ts +181 -0
- package/dist/adapters/deep-agents.js +17 -0
- package/dist/adapters/express.cjs +1253 -0
- package/dist/adapters/express.d.cts +66 -0
- package/dist/adapters/express.d.ts +66 -0
- package/dist/adapters/express.js +12 -0
- package/dist/adapters/index.cjs +2112 -0
- package/dist/adapters/index.d.cts +8 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.js +68 -0
- package/dist/adapters/langchain.cjs +1315 -0
- package/dist/adapters/langchain.d.cts +89 -0
- package/dist/adapters/langchain.d.ts +89 -0
- package/dist/adapters/langchain.js +17 -0
- package/dist/adapters/openai.cjs +1345 -0
- package/dist/adapters/openai.d.cts +99 -0
- package/dist/adapters/openai.d.ts +99 -0
- package/dist/adapters/openai.js +17 -0
- package/dist/adapters/openclaw.cjs +1337 -0
- package/dist/adapters/openclaw.d.cts +99 -0
- package/dist/adapters/openclaw.d.ts +99 -0
- package/dist/adapters/openclaw.js +17 -0
- package/dist/add-ROOZLU62.js +314 -0
- package/dist/behavioral-MJO34S6Q.js +118 -0
- package/dist/bootstrap-CQRZVOXK.js +116 -0
- package/dist/bootstrap-emitter-Q7UIJZ2O.js +7 -0
- package/dist/bootstrap-parser-EEF36XDU.js +7 -0
- package/dist/browser.global.js +941 -0
- package/dist/build-QKOBBC23.js +341 -0
- package/dist/chunk-3WQLXYTP.js +91 -0
- package/dist/chunk-4FLICVVA.js +119 -0
- package/dist/chunk-4NGDRRQH.js +10 -0
- package/dist/chunk-5TPFNWRU.js +215 -0
- package/dist/chunk-5U2MQO5P.js +57 -0
- package/dist/chunk-6CZSKEY5.js +164 -0
- package/dist/chunk-6S5CFQXY.js +624 -0
- package/dist/chunk-7P3S7MAY.js +1090 -0
- package/dist/chunk-A5W4GNQO.js +130 -0
- package/dist/chunk-A7GKPPU7.js +226 -0
- package/dist/chunk-AKW5YVCE.js +96 -0
- package/dist/chunk-B6OXJLJ5.js +622 -0
- package/dist/chunk-BNKJPUPQ.js +113 -0
- package/dist/chunk-BQZMOEML.js +43 -0
- package/dist/chunk-CNSO6XW5.js +207 -0
- package/dist/chunk-CTZHONLA.js +135 -0
- package/dist/chunk-D2UCV5AK.js +326 -0
- package/dist/chunk-EMQDLDAF.js +458 -0
- package/dist/chunk-F66BVUYB.js +340 -0
- package/dist/chunk-G7DJ6VOD.js +101 -0
- package/dist/chunk-I3RRAYK2.js +11 -0
- package/dist/chunk-IS4WUH6Y.js +363 -0
- package/dist/chunk-MH7BT4VH.js +15 -0
- package/dist/chunk-O5ABKEA7.js +304 -0
- package/dist/chunk-OT6PXH54.js +61 -0
- package/dist/chunk-PVTQQS3Y.js +186 -0
- package/dist/chunk-Q6O7ZLO2.js +62 -0
- package/dist/chunk-QLPTHTVB.js +253 -0
- package/dist/chunk-QWGCMQQD.js +16 -0
- package/dist/chunk-QXBFT7NI.js +201 -0
- package/dist/chunk-TG6SEF24.js +246 -0
- package/dist/chunk-U6U7EJZL.js +177 -0
- package/dist/chunk-W7LLXRGY.js +830 -0
- package/dist/chunk-ZJTDUCC2.js +194 -0
- package/dist/chunk-ZWI3NIXK.js +314 -0
- package/dist/cli/neuroverse.cjs +14191 -0
- package/dist/cli/neuroverse.d.cts +1 -0
- package/dist/cli/neuroverse.d.ts +1 -0
- package/dist/cli/neuroverse.js +227 -0
- package/dist/cli/plan.cjs +2439 -0
- package/dist/cli/plan.d.cts +20 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/plan.js +353 -0
- package/dist/cli/run.cjs +2001 -0
- package/dist/cli/run.d.cts +20 -0
- package/dist/cli/run.d.ts +20 -0
- package/dist/cli/run.js +143 -0
- package/dist/configure-ai-6TZ3MCSI.js +132 -0
- package/dist/decision-flow-M63D47LO.js +61 -0
- package/dist/demo-G43RLCPK.js +469 -0
- package/dist/derive-FJZVIPUZ.js +153 -0
- package/dist/doctor-6BC6X2VO.js +173 -0
- package/dist/equity-penalties-SG5IZQ7I.js +244 -0
- package/dist/explain-RHBU2GBR.js +51 -0
- package/dist/guard-AJCCGZMF.js +92 -0
- package/dist/guard-contract-DqFcTScd.d.cts +821 -0
- package/dist/guard-contract-DqFcTScd.d.ts +821 -0
- package/dist/guard-engine-PNR6MHCM.js +10 -0
- package/dist/impact-3XVDSCBU.js +59 -0
- package/dist/improve-TQP4ECSY.js +66 -0
- package/dist/index.cjs +7591 -0
- package/dist/index.d.cts +2195 -0
- package/dist/index.d.ts +2195 -0
- package/dist/index.js +472 -0
- package/dist/infer-world-IFXCACJ5.js +543 -0
- package/dist/init-FYPV4SST.js +144 -0
- package/dist/init-world-TI7ARHBT.js +223 -0
- package/dist/mcp-server-5Y3ZM7TV.js +13 -0
- package/dist/model-adapter-VXEKB4LS.js +11 -0
- package/dist/playground-VZBNPPBO.js +560 -0
- package/dist/redteam-MZPZD3EF.js +357 -0
- package/dist/session-JYOARW54.js +15 -0
- package/dist/shared-7RLUHNMU.js +16 -0
- package/dist/shared-B8dvUUD8.d.cts +60 -0
- package/dist/shared-Dr5Wiay8.d.ts +60 -0
- package/dist/simulate-LJXYBC6M.js +83 -0
- package/dist/test-BOOR4A5F.js +217 -0
- package/dist/trace-PKV4KX56.js +166 -0
- package/dist/validate-RALX7CZS.js +81 -0
- package/dist/validate-engine-7ZXFVGF2.js +7 -0
- package/dist/viz/assets/index-B8SaeJZZ.js +23 -0
- package/dist/viz/index.html +23 -0
- package/dist/world-BIP4GZBZ.js +376 -0
- package/dist/world-loader-Y6HMQH2D.js +13 -0
- package/dist/worlds/autoresearch.nv-world.md +230 -0
- package/dist/worlds/coding-agent.nv-world.md +211 -0
- package/dist/worlds/derivation-world.nv-world.md +278 -0
- package/dist/worlds/research-agent.nv-world.md +169 -0
- package/dist/worlds/social-media.nv-world.md +198 -0
- package/dist/worlds/trading-agent.nv-world.md +218 -0
- package/examples/social-media-sim/bridge.py +209 -0
- package/examples/social-media-sim/simulation.py +927 -0
- package/package.json +16 -3
- 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
|
+
)
|