hypermind 0.11.0__py3-none-any.whl

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 (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,122 @@
1
+ [
2
+ {
3
+ "id": "geo-001",
4
+ "query": "Will armed conflict between two major powers begin within 30 days given mobilisation of 50,000+ troops near a disputed border?",
5
+ "ground_truth": 0.65,
6
+ "category": "kinetic"
7
+ },
8
+ {
9
+ "id": "geo-002",
10
+ "query": "Is satellite imagery showing tank columns near a border consistent with invasion preparation rather than routine exercise?",
11
+ "ground_truth": 0.75,
12
+ "category": "kinetic"
13
+ },
14
+ {
15
+ "id": "geo-003",
16
+ "query": "Will civilian casualties exceed 1,000 in the next 7 days following such mobilisation?",
17
+ "ground_truth": 0.55,
18
+ "category": "kinetic"
19
+ },
20
+ {
21
+ "id": "geo-004",
22
+ "query": "Is diplomatic de-escalation viable when both parties have already broken off ambassadorial relations?",
23
+ "ground_truth": 0.30,
24
+ "category": "diplomatic"
25
+ },
26
+ {
27
+ "id": "geo-005",
28
+ "query": "Will a UN Security Council resolution pass condemning the aggression?",
29
+ "ground_truth": 0.40,
30
+ "category": "diplomatic"
31
+ },
32
+ {
33
+ "id": "geo-006",
34
+ "query": "Will currency markets price in war risk via a >5% drop in the affected nation's currency within 7 days?",
35
+ "ground_truth": 0.70,
36
+ "category": "economic"
37
+ },
38
+ {
39
+ "id": "geo-007",
40
+ "query": "Will sovereign CDS spreads widen to >300bps within 14 days of escalation?",
41
+ "ground_truth": 0.65,
42
+ "category": "economic"
43
+ },
44
+ {
45
+ "id": "geo-008",
46
+ "query": "Will sanctions be imposed within 30 days of the first kinetic action?",
47
+ "ground_truth": 0.85,
48
+ "category": "policy"
49
+ },
50
+ {
51
+ "id": "geo-009",
52
+ "query": "Will mass civilian displacement exceed 100,000 in the first week?",
53
+ "ground_truth": 0.50,
54
+ "category": "humanitarian"
55
+ },
56
+ {
57
+ "id": "geo-010",
58
+ "query": "Will critical infrastructure (power grid, water) be deliberately targeted in the first 14 days?",
59
+ "ground_truth": 0.45,
60
+ "category": "kinetic"
61
+ },
62
+ {
63
+ "id": "geo-011",
64
+ "query": "Will a third-party state intervene militarily within 60 days?",
65
+ "ground_truth": 0.15,
66
+ "category": "kinetic"
67
+ },
68
+ {
69
+ "id": "geo-012",
70
+ "query": "Will a cyberattack on financial infrastructure precede or accompany kinetic operations?",
71
+ "ground_truth": 0.55,
72
+ "category": "cyber"
73
+ },
74
+ {
75
+ "id": "geo-013",
76
+ "query": "Will there be a credible nuclear-doctrine signal (alert posture, treaty withdrawal) in the conflict's first month?",
77
+ "ground_truth": 0.20,
78
+ "category": "kinetic"
79
+ },
80
+ {
81
+ "id": "geo-014",
82
+ "query": "Will the conflict last longer than 90 days?",
83
+ "ground_truth": 0.65,
84
+ "category": "kinetic"
85
+ },
86
+ {
87
+ "id": "geo-015",
88
+ "query": "Will a ceasefire mediated by a neutral state be agreed within 60 days?",
89
+ "ground_truth": 0.25,
90
+ "category": "diplomatic"
91
+ },
92
+ {
93
+ "id": "geo-016",
94
+ "query": "Will domestic protests in the aggressor state exceed 100,000 participants in any single city?",
95
+ "ground_truth": 0.30,
96
+ "category": "domestic"
97
+ },
98
+ {
99
+ "id": "geo-017",
100
+ "query": "Will commodity prices (oil, wheat) spike >15% within 30 days?",
101
+ "ground_truth": 0.55,
102
+ "category": "economic"
103
+ },
104
+ {
105
+ "id": "geo-018",
106
+ "query": "Will a NATO Article 4 consultation be triggered within 14 days?",
107
+ "ground_truth": 0.40,
108
+ "category": "policy"
109
+ },
110
+ {
111
+ "id": "geo-019",
112
+ "query": "Will the International Criminal Court open a formal investigation within 60 days?",
113
+ "ground_truth": 0.55,
114
+ "category": "policy"
115
+ },
116
+ {
117
+ "id": "geo-020",
118
+ "query": "Will a regime-change attempt occur in the aggressor state within 12 months of the conflict's start?",
119
+ "ground_truth": 0.10,
120
+ "category": "domestic"
121
+ }
122
+ ]
@@ -0,0 +1,67 @@
1
+ [
2
+ {
3
+ "id": "gov-001",
4
+ "label": "High-risk classifier",
5
+ "model_card": {
6
+ "model_name": "credit-decision-classifier-v2",
7
+ "intended_use": "Score loan applications for retail banking; outputs feed an underwriter dashboard.",
8
+ "risk_tier": "high",
9
+ "dataset_provenance": "internal application history 2018-2024, documented",
10
+ "mitigations": ["fairness-audit-q3", "appeal-channel", "human-in-the-loop"],
11
+ "deployment_scope": "b2b-customer",
12
+ "autonomous_action": false
13
+ }
14
+ },
15
+ {
16
+ "id": "gov-002",
17
+ "label": "Mass-personalisation LLM",
18
+ "model_card": {
19
+ "model_name": "story-personalisation-llm",
20
+ "intended_use": "Generate personalised marketing copy variants at scale.",
21
+ "risk_tier": "limited",
22
+ "dataset_provenance": "publicly licensed corpora + synthetic augmentations",
23
+ "mitigations": ["rate-limit-100rpm", "content-filter"],
24
+ "deployment_scope": "public-api",
25
+ "autonomous_action": false
26
+ }
27
+ },
28
+ {
29
+ "id": "gov-003",
30
+ "label": "Clinical decision support",
31
+ "model_card": {
32
+ "model_name": "ddx-helper-research",
33
+ "intended_use": "Surface differential-diagnosis options and supporting evidence to clinicians.",
34
+ "risk_tier": "high",
35
+ "dataset_provenance": "MIMIC-IV (research IRB), licensed PubMed abstracts",
36
+ "mitigations": ["clinician-review-required", "exemption-§3060", "audit-log"],
37
+ "deployment_scope": "internal-only",
38
+ "autonomous_action": false
39
+ }
40
+ },
41
+ {
42
+ "id": "gov-004",
43
+ "label": "Autonomous action agent",
44
+ "model_card": {
45
+ "model_name": "auto-refund-agent-v1",
46
+ "intended_use": "Process small-amount refunds end-to-end without human review.",
47
+ "risk_tier": "high",
48
+ "dataset_provenance": "",
49
+ "mitigations": ["amount-cap-50usd"],
50
+ "deployment_scope": "consumer-app",
51
+ "autonomous_action": true
52
+ }
53
+ },
54
+ {
55
+ "id": "gov-005",
56
+ "label": "Children's data product",
57
+ "model_card": {
58
+ "model_name": "edu-tutor-k12",
59
+ "intended_use": "Personalised tutoring assistant for K-12 students.",
60
+ "risk_tier": "high",
61
+ "dataset_provenance": "COPPA-compliant licensed corpora, documented",
62
+ "mitigations": ["coppa-review", "age-gate", "no-pii-retention", "human-review-of-flagged"],
63
+ "deployment_scope": "consumer-app",
64
+ "autonomous_action": false
65
+ }
66
+ }
67
+ ]
@@ -0,0 +1,62 @@
1
+ [
2
+ {
3
+ "id": "msft-001",
4
+ "query": "Will Microsoft's stock price be HIGHER 12 months from now than today?",
5
+ "ground_truth": 0.62,
6
+ "category": "price"
7
+ },
8
+ {
9
+ "id": "msft-002",
10
+ "query": "Will Azure cloud revenue growth remain ABOVE 25% year-over-year over the next four quarters?",
11
+ "ground_truth": 0.58,
12
+ "category": "fundamentals"
13
+ },
14
+ {
15
+ "id": "msft-003",
16
+ "query": "Will Microsoft generate positive ROI on its AI capex (≥$50B committed) within the next 24 months?",
17
+ "ground_truth": 0.55,
18
+ "category": "ai"
19
+ },
20
+ {
21
+ "id": "msft-004",
22
+ "query": "Will Microsoft face a MAJOR antitrust action (formal proceedings, blocked deal, or fine >$1B) within 18 months?",
23
+ "ground_truth": 0.32,
24
+ "category": "regulatory"
25
+ },
26
+ {
27
+ "id": "msft-005",
28
+ "query": "Will Microsoft's operating margin remain ABOVE 40% over the next four quarters?",
29
+ "ground_truth": 0.72,
30
+ "category": "fundamentals"
31
+ },
32
+ {
33
+ "id": "msft-006",
34
+ "query": "Will Microsoft outperform the S&P 500 over the next 12 months?",
35
+ "ground_truth": 0.55,
36
+ "category": "price"
37
+ },
38
+ {
39
+ "id": "msft-007",
40
+ "query": "Will Copilot reach 50M paid seats within 18 months?",
41
+ "ground_truth": 0.60,
42
+ "category": "ai"
43
+ },
44
+ {
45
+ "id": "msft-008",
46
+ "query": "Will Microsoft acquire a major AI company ($10B+) in the next 24 months?",
47
+ "ground_truth": 0.30,
48
+ "category": "m&a"
49
+ },
50
+ {
51
+ "id": "msft-009",
52
+ "query": "Will Microsoft's relationship with OpenAI fundamentally restructure (replaced by exclusive license, equity-only stake, or termination) within 24 months?",
53
+ "ground_truth": 0.40,
54
+ "category": "ai"
55
+ },
56
+ {
57
+ "id": "msft-010",
58
+ "query": "Will Microsoft initiate a major buyback program (>$50B authorisation) in the next 18 months?",
59
+ "ground_truth": 0.55,
60
+ "category": "capital"
61
+ }
62
+ ]
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "id": "wif-001",
4
+ "label": "UAE leaves OPEC by end of 2026",
5
+ "payload": {
6
+ "hypothesis": "UAE leaves OPEC by end of 2026.",
7
+ "horizon_days": 365,
8
+ "drivers": [
9
+ "oil_price_usd=60|80|100",
10
+ "us_saudi_relations=warm|cool|cold"
11
+ ]
12
+ }
13
+ },
14
+ {
15
+ "id": "wif-002",
16
+ "label": "Fed pivots within 6 months",
17
+ "payload": {
18
+ "hypothesis": "The Federal Reserve pivots to rate cuts within 6 months.",
19
+ "horizon_days": 180,
20
+ "drivers": [
21
+ "core_inflation_yoy=below_2|2_to_3|above_3",
22
+ "labor_market=soft|balanced|tight"
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ "id": "wif-003",
28
+ "label": "Major cyber-incident at a named cloud",
29
+ "payload": {
30
+ "hypothesis": "A nation-state cyber-incident affects a major cloud provider within 12 months.",
31
+ "horizon_days": 365,
32
+ "drivers": [
33
+ "actor_capability=nation_state|criminal|insider",
34
+ "blast_radius=single_tenant|regional|global"
35
+ ]
36
+ }
37
+ }
38
+ ]
@@ -0,0 +1,325 @@
1
+ """Sim registry — persistent on-disk record of every simulation run.
2
+
3
+ Layout (under ``$HYPERMIND_HOME/sims/`` or ``~/.hypermind/sims/``):
4
+
5
+ {sim_id}/
6
+ config.json — what was requested (SimConfig.to_dict())
7
+ trace.json — what was produced (full Trace)
8
+ status.json — lifecycle: queued|running|done|failed + progress
9
+ stderr.log — captured stderr from the run
10
+
11
+ A `sim_id` is ``YYYY-MM-DDTHH-MM-SS_xxxxxx`` — sortable lexicographically,
12
+ collision-resistant via a 6-char nonce.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import secrets
20
+ import time
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+
26
+ def home() -> Path:
27
+ """Resolve the SimLab home dir.
28
+
29
+ Priority:
30
+ 1. ``HYPERMIND_HOME`` environment variable
31
+ 2. ``~/.hypermind``
32
+ """
33
+ raw = os.environ.get("HYPERMIND_HOME", "").strip()
34
+ if raw:
35
+ return Path(raw).expanduser()
36
+ return Path.home() / ".hypermind"
37
+
38
+
39
+ def sims_dir() -> Path:
40
+ d = home() / "sims"
41
+ d.mkdir(parents=True, exist_ok=True)
42
+ return d
43
+
44
+
45
+ #: Crockford base32 alphabet — no I/L/O/U so the slug stays unambiguous.
46
+ _B32 = "0123456789abcdefghjkmnpqrstvwxyz"
47
+
48
+
49
+ def _b32encode(n: int, length: int) -> str:
50
+ """Tiny base32 encoder — produces `length` chars from the low bits of n."""
51
+ out: list[str] = []
52
+ for _ in range(length):
53
+ out.append(_B32[n & 0x1F])
54
+ n >>= 5
55
+ return "".join(reversed(out))
56
+
57
+
58
+ def new_sim_id() -> str:
59
+ """Generate a short ULID-style sim slug.
60
+
61
+ Format: ``YYYY-MM-DD-XXXXXXXXXX`` (10-char base32 random tail).
62
+
63
+ - Date prefix keeps directories scannable in `ls` by day.
64
+ - The 50-bit base32 tail is monotonic-within-process (millisecond
65
+ timestamp + a 30-bit random nonce) so IDs sort by creation time
66
+ *within* a day too — the same property UUIDv7 guarantees.
67
+ - 18 chars total, URL-safe (no `/` `:` `+` to escape), readable.
68
+
69
+ Crockford base32 alphabet (no I/L/O/U) — fewer transcription errors
70
+ if a user reads an id off a screen.
71
+
72
+ """
73
+ now_ms = int(time.time() * 1000)
74
+ today = time.strftime("%Y-%m-%d", time.gmtime(now_ms / 1000))
75
+ # 50-bit tail = 30 bits of millis-since-midnight (covers 24h) ‖ 20
76
+ # bits of fresh entropy. Putting millis in the HIGH bits is what
77
+ # makes lexicographic sort match creation order — the same property
78
+ # UUIDv7 / ULID guarantee. 20 bits of random tail = ~1M nonces per
79
+ # millisecond before a collision is even probable.
80
+ midnight = int(now_ms / 86_400_000) * 86_400_000
81
+ ms_today = (now_ms - midnight) & ((1 << 30) - 1)
82
+ rand = secrets.randbits(20)
83
+ tail = _b32encode((ms_today << 20) | rand, 10)
84
+ return f"{today}-{tail}"
85
+
86
+
87
+ def new_sim_dir() -> tuple[str, Path]:
88
+ """Allocate a fresh sim directory; returns (sim_id, path)."""
89
+ sim_id = new_sim_id()
90
+ path = sims_dir() / sim_id
91
+ path.mkdir(parents=True, exist_ok=False)
92
+ return sim_id, path
93
+
94
+
95
+ def sim_dir(sim_id: str) -> Path:
96
+ path = sims_dir() / sim_id
97
+ resolved = path.resolve()
98
+ base = sims_dir().resolve()
99
+ if not str(resolved).startswith(str(base)):
100
+ raise ValueError(f"sim_id escapes sims directory: {sim_id!r}")
101
+ return path
102
+
103
+
104
+ # ---- atomic write helpers --------------------------------------------------
105
+
106
+
107
+ def _write_json_atomic(path: Path, data: Any, mode: int | None = None) -> None:
108
+ tmp = path.with_suffix(path.suffix + ".tmp")
109
+ tmp.write_text(json.dumps(data, indent=2, default=str))
110
+ if mode is not None:
111
+ import os
112
+
113
+ os.chmod(tmp, mode) # chmod before rename to avoid TOCTOU window
114
+ tmp.replace(path) # POSIX atomic rename
115
+
116
+
117
+ def write_config(sim_id: str, cfg_dict: dict[str, Any]) -> None:
118
+ _write_json_atomic(sim_dir(sim_id) / "config.json", cfg_dict)
119
+
120
+
121
+ def load_config(sim_id: str) -> dict[str, Any]:
122
+ p = sim_dir(sim_id) / "config.json"
123
+ if not p.exists():
124
+ raise FileNotFoundError(f"no config for sim {sim_id}")
125
+ return json.loads(p.read_text())
126
+
127
+
128
+ def write_status(sim_id: str, status: dict[str, Any]) -> None:
129
+ _write_json_atomic(sim_dir(sim_id) / "status.json", status)
130
+
131
+
132
+ def load_status(sim_id: str) -> dict[str, Any]:
133
+ p = sim_dir(sim_id) / "status.json"
134
+ if not p.exists():
135
+ return {"sim_id": sim_id, "state": "unknown"}
136
+ return json.loads(p.read_text())
137
+
138
+
139
+ def update_status(sim_id: str, **fields: Any) -> dict[str, Any]:
140
+ """Read-modify-write the status.json for a sim."""
141
+ status = load_status(sim_id)
142
+ status.update(fields)
143
+ write_status(sim_id, status)
144
+ return status
145
+
146
+
147
+ def load_trace(sim_id: str) -> dict[str, Any] | None:
148
+ p = sim_dir(sim_id) / "trace.json"
149
+ if not p.exists():
150
+ return None
151
+ return json.loads(p.read_text())
152
+
153
+
154
+ # ---- listing ---------------------------------------------------------------
155
+
156
+
157
+ @dataclass
158
+ class SimSummary:
159
+ sim_id: str
160
+ state: str
161
+ started_at: float | None
162
+ finished_at: float | None
163
+ n_agents: int | None
164
+ n_questions: int | None
165
+ ciq_score: float | None
166
+ eig_mean: float | None # S3 — primary headline metric (bits/question)
167
+ model: str | None
168
+ enable_tools: bool | None
169
+ deliberation_rounds: int | None
170
+ no_llm: bool | None
171
+ error: str | None
172
+ topic: str | None # user-entered free-text topic (or empty if saved-set was used)
173
+ questions_set: str | None # legacy saved-set name when no topic
174
+
175
+ def to_dict(self) -> dict[str, Any]:
176
+ return {
177
+ "sim_id": self.sim_id,
178
+ "state": self.state,
179
+ "started_at": self.started_at,
180
+ "finished_at": self.finished_at,
181
+ "n_agents": self.n_agents,
182
+ "n_questions": self.n_questions,
183
+ "ciq_score": self.ciq_score,
184
+ "eig_mean": self.eig_mean,
185
+ "model": self.model,
186
+ "enable_tools": self.enable_tools,
187
+ "deliberation_rounds": self.deliberation_rounds,
188
+ "no_llm": self.no_llm,
189
+ "error": self.error,
190
+ "topic": self.topic,
191
+ "questions_set": self.questions_set,
192
+ }
193
+
194
+
195
+ def _summary_from_dir(d: Path) -> SimSummary | None:
196
+ sim_id = d.name
197
+ if not d.is_dir():
198
+ return None
199
+ status = load_status(sim_id)
200
+ config_path = d / "config.json"
201
+ cfg: dict[str, Any] = {}
202
+ if config_path.exists():
203
+ try:
204
+ cfg = json.loads(config_path.read_text())
205
+ except json.JSONDecodeError:
206
+ cfg = {}
207
+ # n_agents, n_questions, ciq_score, eig_mean are written to status.json
208
+ # at completion by the runner — no need to read trace.json for list views.
209
+ return SimSummary(
210
+ sim_id=sim_id,
211
+ state=status.get("state", "unknown"),
212
+ started_at=status.get("started_at"),
213
+ finished_at=status.get("finished_at"),
214
+ n_agents=status.get("n_agents") or cfg.get("personas", 0) * cfg.get("replicas", 0),
215
+ n_questions=status.get("n_questions"),
216
+ ciq_score=status.get("ciq_score"),
217
+ eig_mean=status.get("eig_mean"),
218
+ model=cfg.get("model"),
219
+ enable_tools=cfg.get("enable_tools"),
220
+ deliberation_rounds=cfg.get("deliberation_rounds"),
221
+ no_llm=cfg.get("no_llm"),
222
+ error=status.get("error"),
223
+ topic=cfg.get("topic") or None,
224
+ questions_set=cfg.get("questions") or None,
225
+ )
226
+
227
+
228
+ def list_sims(*, limit: int | None = None) -> list[SimSummary]:
229
+ """Return summaries newest-first. Skips empty/corrupt directories."""
230
+ out: list[SimSummary] = []
231
+ for d in sorted(sims_dir().iterdir(), reverse=True):
232
+ if d.is_dir():
233
+ try:
234
+ s = _summary_from_dir(d)
235
+ if s is not None:
236
+ out.append(s)
237
+ except Exception:
238
+ continue
239
+ if limit is not None and len(out) >= limit:
240
+ break
241
+ return out
242
+
243
+
244
+ def _summary_from_backend(backend: Any, sim_id: str) -> SimSummary | None:
245
+ """Build a SimSummary from a StorageBackend (server tier).
246
+
247
+ Reads status.json and config.json from the backend, and reads the
248
+ top-level summary fields from trace.json when state == "done".
249
+ Returns None if the sim has no status record.
250
+ """
251
+ raw_status = backend.read_file(sim_id, "status.json")
252
+ if raw_status is None:
253
+ return None
254
+ try:
255
+ status: dict[str, Any] = json.loads(raw_status)
256
+ except (json.JSONDecodeError, ValueError):
257
+ return None
258
+
259
+ cfg: dict[str, Any] = {}
260
+ raw_cfg = backend.read_file(sim_id, "config.json")
261
+ if raw_cfg is not None:
262
+ try:
263
+ cfg = json.loads(raw_cfg)
264
+ except (json.JSONDecodeError, ValueError):
265
+ cfg = {}
266
+
267
+ trace_summary: dict[str, Any] = {}
268
+ if status.get("state") == "done":
269
+ trace = backend.read_trace(sim_id) if hasattr(backend, "read_trace") else None
270
+ if trace is not None:
271
+ try:
272
+ trace_summary = {
273
+ "n_agents": trace.get("n_agents"),
274
+ "n_questions": trace.get("n_questions"),
275
+ "ciq_score": (trace.get("collective_iq") or {}).get("score"),
276
+ "eig_mean": (trace.get("collective_iq") or {}).get("eig_mean"),
277
+ }
278
+ except Exception:
279
+ trace_summary = {}
280
+
281
+ return SimSummary(
282
+ sim_id=sim_id,
283
+ state=status.get("state", "unknown"),
284
+ started_at=status.get("started_at"),
285
+ finished_at=status.get("finished_at"),
286
+ n_agents=trace_summary.get("n_agents") or cfg.get("personas", 0) * cfg.get("replicas", 0),
287
+ n_questions=trace_summary.get("n_questions"),
288
+ ciq_score=trace_summary.get("ciq_score"),
289
+ eig_mean=trace_summary.get("eig_mean"),
290
+ model=cfg.get("model"),
291
+ enable_tools=cfg.get("enable_tools"),
292
+ deliberation_rounds=cfg.get("deliberation_rounds"),
293
+ no_llm=cfg.get("no_llm"),
294
+ error=status.get("error"),
295
+ topic=cfg.get("topic") or None,
296
+ questions_set=cfg.get("questions") or None,
297
+ )
298
+
299
+
300
+ def list_sims_for_backend(backend: Any, *, limit: int | None = None) -> list[SimSummary]:
301
+ """Return SimSummary list from a StorageBackend (server tier).
302
+
303
+ Accepts any object that implements ``list_sim_ids() -> list[str]``
304
+ and ``read_file(sim_id, filename) -> bytes | None``. The local
305
+ disk-walk path (``list_sims()``) is unaffected.
306
+ """
307
+ out: list[SimSummary] = []
308
+ for sim_id in backend.list_sim_ids():
309
+ try:
310
+ s = _summary_from_backend(backend, sim_id)
311
+ if s is not None:
312
+ out.append(s)
313
+ except Exception:
314
+ continue
315
+ if limit is not None and len(out) >= limit:
316
+ break
317
+ return out
318
+
319
+
320
+ def append_stderr(sim_id: str, line: str) -> None:
321
+ p = sim_dir(sim_id) / "stderr.log"
322
+ with p.open("a") as f:
323
+ f.write(line)
324
+ if not line.endswith("\n"):
325
+ f.write("\n")