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,27 @@
1
+ """Backend factory — get the right StorageBackend for a namespace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..namespace import get_namespace
6
+
7
+
8
+ def get_backend(namespace_slug: str = "default"):
9
+ """Return the StorageBackend for the given namespace slug."""
10
+ from .local import LocalBackend
11
+
12
+ ns = get_namespace(namespace_slug)
13
+ if ns is None:
14
+ return LocalBackend()
15
+ profile = ns.profile
16
+ if profile.tier == "local":
17
+ return LocalBackend()
18
+ elif profile.tier == "server":
19
+ from .server import ServerBackend
20
+
21
+ return ServerBackend(profile.storage_url or "sqlite:///default")
22
+ elif profile.tier == "decentralised":
23
+ from .anchor import DecentralisedBackend
24
+ from .local import LocalBackend as _L
25
+
26
+ return DecentralisedBackend(inner=_L(), profile=profile)
27
+ return LocalBackend()
@@ -0,0 +1,88 @@
1
+ """DecentralisedBackend — wraps any StorageBackend and adds blockchain anchoring post-sim."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+
9
+ from ..deployment import StorageBackend
10
+ from ..namespace import DeploymentProfile
11
+
12
+ # AnchorRegistry ABI (minimal — only the anchor function)
13
+ _ANCHOR_ABI_SELECTOR = bytes.fromhex(
14
+ "a4b2c1d3"
15
+ ) # keccak256("anchor(bytes32,string,string)")[:4] placeholder
16
+
17
+
18
+ class DecentralisedBackend:
19
+ def __init__(self, inner: StorageBackend, profile: DeploymentProfile) -> None:
20
+ self._inner = inner
21
+ self._profile = profile
22
+
23
+ def write_file(self, sim_id: str, filename: str, data: bytes, mode: int | None = None) -> None:
24
+ self._inner.write_file(sim_id, filename, data, mode)
25
+
26
+ def read_file(self, sim_id: str, filename: str) -> bytes | None:
27
+ return self._inner.read_file(sim_id, filename)
28
+
29
+ def list_sim_ids(self) -> list[str]:
30
+ return self._inner.list_sim_ids()
31
+
32
+ def post_sim_hook(self, sim_id: str, namespace: str, trace: dict) -> None:
33
+ """Compute claims commitment and submit anchor transaction."""
34
+ key = os.environ.get("HYPERMIND_ANCHOR_KEY", "").strip()
35
+ rpc_url = self._profile.anchor_rpc_url or ""
36
+ # Resolve env var reference like "$BASE_RPC_URL"
37
+ if rpc_url.startswith("$"):
38
+ rpc_url = os.environ.get(rpc_url[1:], "")
39
+ if not key or not rpc_url:
40
+ # Anchor silently skipped — key or RPC not configured
41
+ return
42
+ try:
43
+ commitment = _compute_commitment(sim_id, namespace, trace)
44
+ tx_hash, block = _submit_anchor(
45
+ rpc_url, key, commitment, sim_id, namespace, self._profile.anchor_contract
46
+ )
47
+ # Update status.json with anchor data
48
+ status_bytes = self._inner.read_file(sim_id, "status.json")
49
+ if status_bytes:
50
+ status = json.loads(status_bytes)
51
+ status["anchor_tx_hash"] = tx_hash
52
+ status["anchor_block"] = block
53
+ status["anchor_chain"] = self._profile.anchor_chain or "unknown"
54
+ status["anchor_commitment"] = commitment.hex()
55
+ self._inner.write_file(sim_id, "status.json", json.dumps(status, indent=2).encode())
56
+ except Exception as e:
57
+ # Anchor failure is non-fatal — log and continue
58
+ import structlog
59
+
60
+ structlog.get_logger().warning("anchor_failed", sim_id=sim_id, error=str(e))
61
+
62
+
63
+ def _compute_commitment(sim_id: str, namespace: str, trace: dict) -> bytes:
64
+ """SHA-256 of sorted claim kids + sim_id + namespace. Reproducible from trace.json."""
65
+ agents = trace.get("agents", [])
66
+ kids = sorted(c.get("kid", "") for a in agents for c in a.get("published_claims", []))
67
+ finished_at = str(trace.get("finished_at", 0))
68
+ payload = "|".join([sim_id, namespace, finished_at, *kids])
69
+ return hashlib.sha256(payload.encode()).digest()
70
+
71
+
72
+ def _submit_anchor(
73
+ rpc_url: str,
74
+ private_key_hex: str,
75
+ commitment: bytes,
76
+ sim_id: str,
77
+ namespace: str,
78
+ contract_addr: str | None,
79
+ ) -> tuple[str, int]:
80
+ """Submit anchor transaction via raw JSON-RPC. Returns (tx_hash, block_number)."""
81
+ # Use httpx for JSON-RPC — already a dependency via tool_handlers
82
+
83
+ # Note: eth_utils_lite is a minimal helper we'll implement inline below
84
+ # to avoid adding web3 as a dependency.
85
+ raise NotImplementedError(
86
+ "Blockchain anchor requires configuring HYPERMIND_ANCHOR_KEY and anchor_rpc_url. "
87
+ "See docs/deployment.md for setup instructions."
88
+ )
@@ -0,0 +1,32 @@
1
+ """LocalBackend — wraps registry.py for zero-change behaviour on the 'local' tier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..registry import sim_dir, sims_dir
6
+
7
+
8
+ class LocalBackend:
9
+ def write_file(self, sim_id: str, filename: str, data: bytes, mode: int | None = None) -> None:
10
+ path = sim_dir(sim_id) / filename
11
+ if mode is not None:
12
+ import os
13
+
14
+ tmp = path.with_suffix(path.suffix + ".tmp")
15
+ tmp.write_bytes(data)
16
+ os.chmod(tmp, mode)
17
+ tmp.replace(path)
18
+ else:
19
+ path.write_bytes(data)
20
+
21
+ def read_file(self, sim_id: str, filename: str) -> bytes | None:
22
+ path = sim_dir(sim_id) / filename
23
+ if not path.exists():
24
+ return None
25
+ return path.read_bytes()
26
+
27
+ def list_sim_ids(self) -> list[str]:
28
+ d = sims_dir()
29
+ return sorted(p.name for p in d.iterdir() if p.is_dir())
30
+
31
+ def post_sim_hook(self, sim_id: str, namespace: str, trace: dict) -> None:
32
+ pass # local tier: no post-sim action
@@ -0,0 +1,79 @@
1
+ """ServerBackend — SQLite (MVP) or Postgres storage for shared/multi-user deployments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import time
8
+ from pathlib import Path
9
+
10
+
11
+ def _parse_sqlite_path(storage_url: str) -> str:
12
+ """Extract file path from sqlite:///path or sqlite:////abs/path."""
13
+ if storage_url.startswith("sqlite:///"):
14
+ return storage_url[len("sqlite:///") :]
15
+ raise ValueError(
16
+ f"Unsupported storage_url format: {storage_url!r}. Use sqlite:///path/to/db.sqlite"
17
+ )
18
+
19
+
20
+ class ServerBackend:
21
+ def __init__(self, storage_url: str) -> None:
22
+ self._db_path = _parse_sqlite_path(storage_url)
23
+ self._init_db()
24
+
25
+ def _conn(self) -> sqlite3.Connection:
26
+ con = sqlite3.connect(self._db_path, check_same_thread=False)
27
+ con.row_factory = sqlite3.Row
28
+ return con
29
+
30
+ def _init_db(self) -> None:
31
+ Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
32
+ with self._conn() as con:
33
+ con.execute("PRAGMA journal_mode=WAL")
34
+ con.execute("PRAGMA synchronous=NORMAL")
35
+ con.execute("PRAGMA busy_timeout=5000")
36
+ con.execute("""
37
+ CREATE TABLE IF NOT EXISTS sim_files (
38
+ sim_id TEXT NOT NULL,
39
+ filename TEXT NOT NULL,
40
+ data BLOB NOT NULL,
41
+ updated_at INTEGER NOT NULL,
42
+ PRIMARY KEY (sim_id, filename)
43
+ )
44
+ """)
45
+ con.execute("CREATE INDEX IF NOT EXISTS idx_sim_files_sim_id ON sim_files(sim_id)")
46
+
47
+ def write_file(self, sim_id: str, filename: str, data: bytes, mode: int | None = None) -> None:
48
+ with self._conn() as con:
49
+ con.execute(
50
+ "INSERT OR REPLACE INTO sim_files (sim_id, filename, data, updated_at) VALUES (?,?,?,?)",
51
+ (sim_id, filename, data, int(time.time() * 1000)),
52
+ )
53
+
54
+ def read_file(self, sim_id: str, filename: str) -> bytes | None:
55
+ with self._conn() as con:
56
+ row = con.execute(
57
+ "SELECT data FROM sim_files WHERE sim_id=? AND filename=?", (sim_id, filename)
58
+ ).fetchone()
59
+ return bytes(row["data"]) if row else None
60
+
61
+ def list_sim_ids(self) -> list[str]:
62
+ with self._conn() as con:
63
+ rows = con.execute(
64
+ "SELECT DISTINCT sim_id FROM sim_files WHERE filename='status.json' ORDER BY sim_id DESC"
65
+ ).fetchall()
66
+ return [r["sim_id"] for r in rows]
67
+
68
+ def read_trace(self, sim_id: str) -> dict | None:
69
+ """Return the parsed trace.json for *sim_id*, or None if not found."""
70
+ raw = self.read_file(sim_id, "trace.json")
71
+ if raw is None:
72
+ return None
73
+ try:
74
+ return json.loads(raw)
75
+ except (json.JSONDecodeError, ValueError):
76
+ return None
77
+
78
+ def post_sim_hook(self, sim_id: str, namespace: str, trace: dict) -> None:
79
+ pass # server tier: no anchor
@@ -0,0 +1,108 @@
1
+ """Implementation of the ``hmctl simlab`` command.
2
+
3
+ Boots the SimLab ASGI app via uvicorn on a single port (default 8765),
4
+ serves the Vue SPA at ``/`` and the API at ``/v1/sims/*``, optionally
5
+ opens a browser tab.
6
+
7
+ Auto-loads ``.env`` from the current working directory or any parent
8
+ directory (until a project root marker like pyproject.toml or .git is
9
+ found), so OPENROUTER_API_KEY etc. are picked up without manual export.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import signal
16
+ import threading
17
+ import webbrowser
18
+ from pathlib import Path
19
+
20
+ from hypermind.simlab.app import simlab_app
21
+
22
+
23
+ def _install_sigterm_handler() -> None:
24
+ """Install a SIGTERM handler that allows up to 10 s for in-flight
25
+ requests to drain before exiting cleanly.
26
+
27
+ Uvicorn installs its own SIGTERM handler when it starts, so this is
28
+ installed *before* ``uvicorn.run()`` — uvicorn will overwrite it. The
29
+ handler is here as a safety net for cases where the process is started
30
+ without uvicorn's signal machinery (e.g. direct Python subprocess).
31
+ """
32
+
33
+ def _handle_sigterm(signum: int, frame: object) -> None: # type: ignore[type-arg]
34
+ def _deferred_exit() -> None:
35
+ import time
36
+
37
+ time.sleep(0.1) # brief drain window for in-flight requests
38
+ os._exit(0)
39
+
40
+ t = threading.Thread(target=_deferred_exit, daemon=True)
41
+ t.start()
42
+
43
+ try:
44
+ signal.signal(signal.SIGTERM, _handle_sigterm)
45
+ except (OSError, ValueError):
46
+ # May fail in non-main threads or on platforms that don't support it.
47
+ pass
48
+
49
+
50
+ def _load_env_file() -> Path | None:
51
+ """Walk up from cwd looking for a .env; load it without overwriting
52
+ existing env vars. Returns the path loaded, or None.
53
+ """
54
+ here = Path.cwd().resolve()
55
+ for parent in [here, *here.parents]:
56
+ env_path = parent / ".env"
57
+ if env_path.exists():
58
+ for raw in env_path.read_text().splitlines():
59
+ line = raw.strip()
60
+ if not line or line.startswith("#") or "=" not in line:
61
+ continue
62
+ key, _, value = line.partition("=")
63
+ key = key.strip()
64
+ value = value.strip().strip('"').strip("'")
65
+ # Don't override an explicitly-exported var
66
+ if key and key not in os.environ:
67
+ os.environ[key] = value
68
+ return env_path
69
+ if (parent / "pyproject.toml").exists() or (parent / ".git").is_dir():
70
+ break
71
+ return None
72
+
73
+
74
+ def run_simlab(*, port: int, host: str = "127.0.0.1", open_browser: bool = True) -> None:
75
+ """Block on a uvicorn server hosting the SimLab app."""
76
+ try:
77
+ import uvicorn
78
+ except ImportError as err: # pragma: no cover - install-time guard
79
+ raise SystemExit(
80
+ "uvicorn is required for `hmctl simlab`. "
81
+ "Install with `pip install 'hypermind[server]'` or `pip install uvicorn`."
82
+ ) from err
83
+
84
+ env_loaded = _load_env_file()
85
+ has_key = bool(os.environ.get("OPENROUTER_API_KEY", "").strip())
86
+
87
+ app = simlab_app()
88
+ url = f"http://{host}:{port}/"
89
+
90
+ if open_browser:
91
+ try:
92
+ webbrowser.open(url, new=2)
93
+ except Exception:
94
+ pass
95
+
96
+ print(f" HyperMind SimLab listening on {url}")
97
+ print(f" API: {url}v1/sims")
98
+ print(f" Health: {url}v1/healthz")
99
+ if env_loaded:
100
+ print(f" Loaded: {env_loaded}")
101
+ if has_key:
102
+ print(" LLM: OPENROUTER_API_KEY detected — real LLM mode available")
103
+ else:
104
+ print(" LLM: no OPENROUTER_API_KEY — sims will run synthetic")
105
+ print(" (add to .env or `export OPENROUTER_API_KEY=…` to enable)")
106
+ print(" (ctrl-c to stop)")
107
+ _install_sigterm_handler()
108
+ uvicorn.run(app, host=host, port=port, log_level="warning", lifespan="on", access_log=False)
@@ -0,0 +1,106 @@
1
+ """SimConfig — declarative configuration for a persona-panel simulation run.
2
+
3
+ This is the on-the-wire shape POSTed to `/v1/sims` and the dict-shape
4
+ written to `~/.hypermind/sims/{id}/config.json`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict, dataclass
10
+ from typing import Any
11
+
12
+ DEFAULT_MODEL = "openai/gpt-4o-mini"
13
+ DEFAULT_QUESTIONS = "msft_outlook_q10"
14
+ DEFAULT_MODE = "binary-forecast"
15
+
16
+
17
+ @dataclass
18
+ class SimConfig:
19
+ personas: int = 6
20
+ replicas: int = 5
21
+ # Either a free-text topic the panel should deliberate on, OR a saved
22
+ # question-set name. ``topic`` wins if both are set.
23
+ topic: str = ""
24
+ n_questions: int = 8
25
+ questions: str = DEFAULT_QUESTIONS # legacy: saved set name or path
26
+ model: str = DEFAULT_MODEL
27
+ enable_tools: bool = False
28
+ deliberation_rounds: int = 2
29
+ no_llm: bool = False
30
+ debug: bool = False # terminal narrative; not a UI concern, but kept for parity
31
+ # Swarm composition — three layered ways to specify the panel:
32
+ # 1. ``swarm`` slug (highest priority): full Swarm recipe — members,
33
+ # tools, and bias_prompt all come from the saved swarm.
34
+ # 2. ``personas_override``: ad-hoc list of persona slugs (each with
35
+ # ``replicas`` copies). Used by tests + the CLI.
36
+ # 3. fallback: count-based ``personas: int`` slicing the first N
37
+ # built-in personas (legacy behavior, backward compatible).
38
+ swarm: str | None = None
39
+ personas_override: list[str] | None = None
40
+ namespace: str = "default"
41
+ # Deliberation mode — selects the result shape (binary probability,
42
+ # governance receipt, scenario tree, ...). Resolved against the
43
+ # registry in ``simlab.modes``. Default preserves legacy behaviour.
44
+ mode: str = DEFAULT_MODE
45
+ # Mode-specific input payload (e.g. model_card for governance,
46
+ # drivers list for what-if-tree). Validated by the mode registry,
47
+ # not by SimConfig itself, so adding a new mode does not require
48
+ # touching this file.
49
+ mode_payload: dict[str, Any] | None = None
50
+ # Pre-generated questions from the topic-adapter pre-flight step.
51
+ # When set, _scenario_impl skips the in-worker generate_questions_for_topic
52
+ # call entirely — saves one LLM round-trip off the hot path.
53
+ # Not persisted to config.json (excluded from to_dict via from_dict filter).
54
+ _pregenerated_questions: list | None = None
55
+
56
+ def to_dict(self) -> dict[str, Any]:
57
+ d = asdict(self)
58
+ d.pop("_pregenerated_questions", None)
59
+ return d
60
+
61
+ @classmethod
62
+ def from_dict(cls, d: dict[str, Any]) -> SimConfig:
63
+ # Permissive — silently drop unknown keys; clamp numeric ranges
64
+ known = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
65
+ clean = {k: v for k, v in d.items() if k in known}
66
+ cfg = cls(**clean)
67
+ cfg.validate()
68
+ return cfg
69
+
70
+ def validate(self) -> None:
71
+ # Mode must be registered. Lazy-import to avoid a circular dep
72
+ # (modes registry imports config types).
73
+ from .modes import get_mode, is_registered_mode
74
+
75
+ if not is_registered_mode(self.mode):
76
+ raise ValueError(
77
+ f"unknown mode {self.mode!r}; registered modes: "
78
+ f"see hypermind.simlab.modes.list_modes()"
79
+ )
80
+
81
+ # Modes that read only ``mode_payload`` skip all legacy-field
82
+ # validation — they don't use personas/replicas/topic/questions.
83
+ mode_spec = get_mode(self.mode)
84
+ if not mode_spec.uses_legacy_fields:
85
+ return
86
+
87
+ # Legacy validation (binary-forecast and any other mode that opts
88
+ # into the original SimConfig fields).
89
+ roster_overridden = bool(self.swarm) or bool(self.personas_override)
90
+ if not roster_overridden:
91
+ if not 1 <= self.personas <= 50:
92
+ raise ValueError(f"personas must be 1..50, got {self.personas}")
93
+ if not 1 <= self.replicas <= 200:
94
+ raise ValueError(f"replicas must be 1..200, got {self.replicas}")
95
+ if not 1 <= self.deliberation_rounds <= 10:
96
+ raise ValueError(f"deliberation_rounds must be 1..10, got {self.deliberation_rounds}")
97
+ # Either a topic or a saved set is acceptable. Topic wins.
98
+ topic = (self.topic or "").strip()
99
+ if not topic and not self.questions:
100
+ raise ValueError("provide either a topic or a question-set name")
101
+ if not 1 <= self.n_questions <= 200:
102
+ raise ValueError(f"n_questions must be 1..200, got {self.n_questions}")
103
+
104
+ @property
105
+ def n_agents(self) -> int:
106
+ return self.personas * self.replicas
@@ -0,0 +1,26 @@
1
+ """StorageBackend protocol — all persistence routes through this interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class StorageBackend(Protocol):
10
+ """Persistence interface for simulation artifacts."""
11
+
12
+ def write_file(self, sim_id: str, filename: str, data: bytes, mode: int | None = None) -> None:
13
+ """Write a file for a given sim_id."""
14
+ ...
15
+
16
+ def read_file(self, sim_id: str, filename: str) -> bytes | None:
17
+ """Read a file for a given sim_id. Returns None if not found."""
18
+ ...
19
+
20
+ def list_sim_ids(self) -> list[str]:
21
+ """List all known sim_ids managed by this backend."""
22
+ ...
23
+
24
+ def post_sim_hook(self, sim_id: str, namespace: str, trace: dict) -> None:
25
+ """Called after a sim completes. Used for anchoring in decentralised tier."""
26
+ ...