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,17 @@
1
+ """Knowledge / reputation / receipt layer."""
2
+
3
+ from hypermind.knowledge.correlated_agreement import Report, score_reports
4
+ from hypermind.knowledge.reputation import ReputationKernel, ReputationSnapshot
5
+ from hypermind.knowledge.transparency import (
6
+ TransparencyService,
7
+ WitnessCosignature,
8
+ )
9
+
10
+ __all__ = [
11
+ "Report",
12
+ "ReputationKernel",
13
+ "ReputationSnapshot",
14
+ "TransparencyService",
15
+ "WitnessCosignature",
16
+ "score_reports",
17
+ ]
@@ -0,0 +1,141 @@
1
+ """Citation-graph queries (ROADMAP §0.3.4).
2
+
3
+ A :class:`CitationGraph` builds a directed acyclic graph (DAG) from
4
+ the ``cites`` field of each claim. Each ClaimEvent's payload may
5
+ include::
6
+
7
+ {"cites": [b"<parent_kid>", ...], ...}
8
+
9
+ The graph supports ancestor / descendant queries with bounded depth
10
+ plus a cycle check (claims may not transitively cite themselves).
11
+
12
+ Cycle detection uses iterative depth-first search with a recursion
13
+ stack; it short-circuits on the first back-edge so the worst-case is
14
+ ``O(V + E)``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Iterable
20
+ from dataclasses import dataclass, field
21
+
22
+ import cbor2
23
+
24
+
25
+ @dataclass
26
+ class CitationGraph:
27
+ """In-memory citation DAG."""
28
+
29
+ #: Forward edges: ``claim_kid → set(parent_kids)``.
30
+ _cites: dict[bytes, set[bytes]] = field(default_factory=dict)
31
+ #: Reverse edges for descendant queries.
32
+ _cited_by: dict[bytes, set[bytes]] = field(default_factory=dict)
33
+
34
+ # ---- ingest ------------------------------------------------------
35
+
36
+ def add(self, claim_kid: bytes, cites: Iterable[bytes] = ()) -> None:
37
+ """Add a claim and its outgoing citations.
38
+
39
+ Idempotent: re-adding the same claim with the same edges is a
40
+ no-op. Re-adding with *new* edges merges them.
41
+ """
42
+ targets = {bytes(c) for c in cites}
43
+ self._cites.setdefault(claim_kid, set()).update(targets)
44
+ for t in targets:
45
+ self._cited_by.setdefault(t, set()).add(claim_kid)
46
+ # Ensure target has a forward-edge entry so it's a known node.
47
+ self._cites.setdefault(t, set())
48
+
49
+ def add_from_payload(self, claim_kid: bytes, payload: bytes) -> None:
50
+ """Convenience: parse `cites` from a CBOR-encoded claim payload."""
51
+ try:
52
+ d = cbor2.loads(payload)
53
+ except Exception:
54
+ return
55
+ cites = d.get("cites", ()) if isinstance(d, dict) else ()
56
+ if isinstance(cites, (list, tuple)):
57
+ self.add(claim_kid, [bytes(c) for c in cites if isinstance(c, (bytes, bytearray))])
58
+ else:
59
+ self.add(claim_kid, ())
60
+
61
+ # ---- queries -----------------------------------------------------
62
+
63
+ def ancestors(self, claim_kid: bytes, *, depth: int = 1) -> set[bytes]:
64
+ """Return ancestors (transitively cited claims) up to ``depth``."""
65
+ if depth <= 0:
66
+ return set()
67
+ seen: set[bytes] = set()
68
+ frontier = {claim_kid}
69
+ for _ in range(depth):
70
+ next_frontier: set[bytes] = set()
71
+ for node in frontier:
72
+ for parent in self._cites.get(node, ()): # cited-by-this
73
+ if parent not in seen:
74
+ seen.add(parent)
75
+ next_frontier.add(parent)
76
+ frontier = next_frontier
77
+ if not frontier:
78
+ break
79
+ seen.discard(claim_kid)
80
+ return seen
81
+
82
+ def descendants(self, claim_kid: bytes, *, depth: int = 1) -> set[bytes]:
83
+ """Return descendants (claims that cite this) up to ``depth``."""
84
+ if depth <= 0:
85
+ return set()
86
+ seen: set[bytes] = set()
87
+ frontier = {claim_kid}
88
+ for _ in range(depth):
89
+ next_frontier: set[bytes] = set()
90
+ for node in frontier:
91
+ for child in self._cited_by.get(node, ()):
92
+ if child not in seen:
93
+ seen.add(child)
94
+ next_frontier.add(child)
95
+ frontier = next_frontier
96
+ if not frontier:
97
+ break
98
+ seen.discard(claim_kid)
99
+ return seen
100
+
101
+ def cycle_check(self) -> list[bytes] | None:
102
+ """Return a list of nodes forming a cycle, or ``None`` if DAG.
103
+
104
+ Iterative DFS with recursion-stack tracking. Short-circuits at
105
+ the first back-edge.
106
+ """
107
+ WHITE, GRAY, BLACK = 0, 1, 2
108
+ color: dict[bytes, int] = {n: WHITE for n in self._cites}
109
+ for start in list(color.keys()):
110
+ if color[start] != WHITE:
111
+ continue
112
+ stack: list[tuple[bytes, list[bytes]]] = [(start, list(self._cites.get(start, ())))]
113
+ path = [start]
114
+ color[start] = GRAY
115
+ while stack:
116
+ node, children = stack[-1]
117
+ if not children:
118
+ color[node] = BLACK
119
+ stack.pop()
120
+ if path and path[-1] == node:
121
+ path.pop()
122
+ continue
123
+ child = children.pop()
124
+ cstate = color.get(child, WHITE)
125
+ if cstate == GRAY:
126
+ # cycle found; reconstruct from path
127
+ if child in path:
128
+ idx = path.index(child)
129
+ return [*path[idx:], child]
130
+ return [child]
131
+ if cstate == WHITE:
132
+ color[child] = GRAY
133
+ stack.append((child, list(self._cites.get(child, ()))))
134
+ path.append(child)
135
+ return None
136
+
137
+ def __len__(self) -> int:
138
+ return len(self._cites)
139
+
140
+ def nodes(self) -> set[bytes]:
141
+ return set(self._cites.keys())
@@ -0,0 +1,167 @@
1
+ """Correlated-Agreement peer prediction (ROADMAP §0.2.2).
2
+
3
+ Implements a Prelec / Krishnamurthy-Mittendorff-style Bayesian Truth
4
+ Serum scoring rule. Each reporter provides:
5
+
6
+ * an ``answer`` (categorical), and
7
+ * a ``prediction`` — a probability distribution over what other
8
+ reporters will answer.
9
+
10
+ The score for reporter *i* is::
11
+
12
+ score_i = log(actual_freq[answer_i] / predicted_freq[answer_i])
13
+ + ALPHA * log(predicted_correctness_i)
14
+
15
+ where ``predicted_correctness_i`` is the geometric-mean agreement
16
+ between reporter *i*'s prediction and the empirical answer
17
+ distribution. The first term rewards "surprisingly common" answers;
18
+ the second term rewards calibrated predictions of the population.
19
+
20
+ The scoring rule is *incentive-compatible* in the limit: truth-telling
21
+ maximises expected score for any prior the reporter may hold. See
22
+ Prelec (Science, 2004) and the Krishnamurthy-Mittendorff
23
+ generalisation for the multi-answer / continuous-prediction case.
24
+
25
+ Wire into :class:`~hypermind.knowledge.reputation.ReputationKernel`
26
+ via :meth:`ReputationKernel.update_with_correlated_agreement`.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import math
32
+ from collections.abc import Iterable, Mapping
33
+ from dataclasses import dataclass, field
34
+
35
+ ALPHA_DEFAULT = 1.0
36
+ EPS = 1e-9
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Report:
41
+ """A single peer report.
42
+
43
+ Attributes:
44
+ reporter_id: Opaque identifier (kid bytes are typical).
45
+ answer: The reporter's chosen answer (categorical).
46
+ prediction: Reporter's estimate of the population's answer
47
+ distribution. Keys MUST be a superset of every answer that
48
+ actually appears among reporters; values must be
49
+ non-negative and sum to ~1.0 (normalised internally).
50
+ """
51
+
52
+ reporter_id: bytes
53
+ answer: str
54
+ prediction: Mapping[str, float] = field(default_factory=dict)
55
+
56
+
57
+ def _normalise(d: Mapping[str, float]) -> dict[str, float]:
58
+ total = sum(max(0.0, v) for v in d.values())
59
+ if total <= 0:
60
+ return {k: 0.0 for k in d}
61
+ return {k: max(0.0, v) / total for k, v in d.items()}
62
+
63
+
64
+ def _empirical_freq(reports: Iterable[Report]) -> dict[str, float]:
65
+ counts: dict[str, float] = {}
66
+ n = 0
67
+ for r in reports:
68
+ counts[r.answer] = counts.get(r.answer, 0.0) + 1.0
69
+ n += 1
70
+ if n == 0:
71
+ return {}
72
+ return {k: v / n for k, v in counts.items()}
73
+
74
+
75
+ def _avg_prediction(reports: list[Report]) -> dict[str, float]:
76
+ """Geometric-mean average of reporters' predictions (BTS canonical).
77
+
78
+ Aggregating predictions in log-space matches Prelec's BTS derivation
79
+ and is well-behaved when individual predictions assign probability 0
80
+ to an answer that another reporter actually picks (the EPS floor).
81
+ """
82
+ if not reports:
83
+ return {}
84
+ keys = set()
85
+ for r in reports:
86
+ keys.update(r.prediction.keys())
87
+ log_acc: dict[str, float] = {k: 0.0 for k in keys}
88
+ for r in reports:
89
+ norm = _normalise({k: r.prediction.get(k, 0.0) for k in keys})
90
+ for k, v in norm.items():
91
+ log_acc[k] += math.log(max(v, EPS))
92
+ n = len(reports)
93
+ log_acc = {k: v / n for k, v in log_acc.items()}
94
+ raw = {k: math.exp(v) for k, v in log_acc.items()}
95
+ return _normalise(raw)
96
+
97
+
98
+ def score_reports(
99
+ reports: list[Report],
100
+ *,
101
+ alpha: float = ALPHA_DEFAULT,
102
+ ) -> dict[bytes, float]:
103
+ """Score each reporter under correlated-agreement / BTS.
104
+
105
+ Returns a mapping from reporter_id (bytes) to score (float). Higher
106
+ is better; calibrated truthful reporters obtain positive scores.
107
+
108
+ The ``alpha`` weight controls the trade-off between "surprisingly
109
+ common" answer reward (information score) and prediction
110
+ calibration. ``alpha=1.0`` is the canonical Prelec choice.
111
+ """
112
+ if not reports:
113
+ return {}
114
+ if len(reports) == 1:
115
+ # BTS is undefined with a single reporter; return a sentinel
116
+ # zero so callers can still feed the result into the reputation
117
+ # kernel without special-casing.
118
+ return {reports[0].reporter_id: 0.0}
119
+
120
+ actual = _empirical_freq(reports)
121
+ predicted = _avg_prediction(reports)
122
+
123
+ # Universe of answers: union of every answer that appears AND every
124
+ # answer any reporter mentioned in its prediction.
125
+ universe = set(actual) | set(predicted)
126
+ for r in reports:
127
+ universe.update(r.prediction.keys())
128
+ # Floor predicted to EPS for any answer that was actually given.
129
+ predicted = {k: max(predicted.get(k, 0.0), EPS) for k in universe}
130
+ # Renormalise after flooring so probabilities still sum to 1.
131
+ predicted = _normalise(predicted)
132
+
133
+ scores: dict[bytes, float] = {}
134
+ for r in reports:
135
+ info_term = math.log(
136
+ max(actual.get(r.answer, 0.0), EPS) / max(predicted.get(r.answer, EPS), EPS)
137
+ )
138
+ # Reporter's predicted-correctness: geometric agreement with
139
+ # the empirical distribution.
140
+ pred_norm = _normalise({k: r.prediction.get(k, 0.0) for k in universe})
141
+ agreement = 0.0
142
+ for k in universe:
143
+ agreement += actual.get(k, 0.0) * math.log(max(pred_norm.get(k, 0.0), EPS))
144
+ # `agreement` is the cross-entropy of actual relative to
145
+ # reporter's prediction — already a log-probability.
146
+ scores[r.reporter_id] = info_term + alpha * agreement
147
+ return scores
148
+
149
+
150
+ def update_kernel_with_reports(
151
+ kernel,
152
+ reports: list[Report],
153
+ *,
154
+ topic: str,
155
+ alpha: float = ALPHA_DEFAULT,
156
+ threshold: float = 0.0,
157
+ ) -> dict[bytes, float]:
158
+ """Apply correlated-agreement scores to a ReputationKernel.
159
+
160
+ Reporters scoring above ``threshold`` are recorded as ``correct``;
161
+ those below as ``incorrect``. The raw score map is returned for
162
+ inspection / tests.
163
+ """
164
+ raw = score_reports(reports, alpha=alpha)
165
+ for kid, score in raw.items():
166
+ kernel.record_outcome(kid, topic, correct=score >= threshold)
167
+ return raw
@@ -0,0 +1,115 @@
1
+ """Namespace-scoped embedding-model registry (ROADMAP §0.4.2).
2
+
3
+ Each namespace registers the set of embedding models its claims may
4
+ reference. A claim that includes ``embedding_model_id`` and
5
+ ``embedding_vector`` fields MUST refer to a registered model; the
6
+ verifier rejects anything else.
7
+
8
+ This closes a class of model-substitution attacks where an attacker
9
+ publishes claims with vectors derived from an unauthenticated model
10
+ (or a different model whose vectors are not comparable to the
11
+ namespace's canonical model).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ from dataclasses import dataclass, field
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class EmbeddingModelEntry:
22
+ """A registered embedding model.
23
+
24
+ Attributes:
25
+ model_id: Caller-chosen identifier (typically a string like
26
+ ``"openai/text-embedding-3-large"``).
27
+ model_hash: 32-byte hash binding the model artifact (e.g.
28
+ SHA-256 of the canonical weights archive). Verifiers MAY
29
+ require this to match a published artifact.
30
+ dim: Vector dimension. Vectors with the wrong length are
31
+ rejected.
32
+ source_url: Optional provenance URL for human auditors.
33
+ """
34
+
35
+ model_id: str
36
+ model_hash: bytes
37
+ dim: int
38
+ source_url: str = ""
39
+
40
+ def __post_init__(self) -> None:
41
+ if not self.model_id:
42
+ raise ValueError("model_id must be non-empty")
43
+ if len(self.model_hash) != 32:
44
+ raise ValueError(f"model_hash must be 32 bytes, got {len(self.model_hash)}")
45
+ if self.dim <= 0:
46
+ raise ValueError("dim must be positive")
47
+
48
+
49
+ @dataclass
50
+ class EmbeddingModelRegistry:
51
+ """Per-namespace embedding-model registry."""
52
+
53
+ namespace_id: bytes = b"\x00" * 32
54
+ _entries: dict[str, EmbeddingModelEntry] = field(default_factory=dict)
55
+
56
+ def register(
57
+ self,
58
+ model_id: str,
59
+ *,
60
+ model_hash: bytes,
61
+ dim: int,
62
+ source_url: str = "",
63
+ ) -> EmbeddingModelEntry:
64
+ """Register a new model. Re-registering an existing ``model_id``
65
+ with different parameters raises :class:`ValueError`."""
66
+ new = EmbeddingModelEntry(
67
+ model_id=model_id,
68
+ model_hash=model_hash,
69
+ dim=dim,
70
+ source_url=source_url,
71
+ )
72
+ existing = self._entries.get(model_id)
73
+ if existing is not None and existing != new:
74
+ raise ValueError(
75
+ f"model_id {model_id!r} already registered with different "
76
+ f"parameters; refusing silent overwrite"
77
+ )
78
+ self._entries[model_id] = new
79
+ return new
80
+
81
+ def get(self, model_id: str) -> EmbeddingModelEntry | None:
82
+ return self._entries.get(model_id)
83
+
84
+ def __contains__(self, model_id: object) -> bool:
85
+ return isinstance(model_id, str) and model_id in self._entries
86
+
87
+ def __len__(self) -> int:
88
+ return len(self._entries)
89
+
90
+ def verify(
91
+ self,
92
+ *,
93
+ model_id: str,
94
+ vector: list[float] | tuple[float, ...] | None,
95
+ ) -> None:
96
+ """Raise :class:`ValueError` if the vector / model ref is invalid.
97
+
98
+ Returns ``None`` on success — convention matches other verifier
99
+ primitives in the SDK.
100
+ """
101
+ entry = self._entries.get(model_id)
102
+ if entry is None:
103
+ raise ValueError(f"embedding model {model_id!r} not registered in this namespace")
104
+ if vector is None:
105
+ raise ValueError("embedding_vector required when embedding_model_id set")
106
+ if len(vector) != entry.dim:
107
+ raise ValueError(
108
+ f"vector dim {len(vector)} mismatches registered dim {entry.dim} "
109
+ f"for model {model_id!r}"
110
+ )
111
+
112
+ @staticmethod
113
+ def hash_artifact(blob: bytes) -> bytes:
114
+ """SHA-256 hash helper for callers building model_hash values."""
115
+ return hashlib.sha256(blob).digest()
@@ -0,0 +1,197 @@
1
+ """S4 — Cross-sim posterior persistence.
2
+
3
+ Goal: the swarm gets smarter *across* sims, not just within a run.
4
+
5
+ Today every ``run_persona_sim`` builds a fresh testbed cohort, and the
6
+ ReputationKernel + BrierFeedback state lives only for that process. That
7
+ makes "the swarm learns" an unverifiable demo claim: each sim is a cold
8
+ start.
9
+
10
+ KernelStore persists per-(persona_slug, topic) posteriors and a compact
11
+ Brier-tracker summary to ``HYPERMIND_HOME/swarm_memory/<persona>.json``.
12
+ The simlab profile builder hydrates ``working_memory["posteriors"]`` from
13
+ disk at sim start, and BrierFeedback writes back at apply-time. Schema-
14
+ versioned for forward compatibility.
15
+
16
+ NOT used for cryptographic state — Beta-posterior summaries are
17
+ operationally useful but not authoritative. The transparency log
18
+ remains the only source of truth for claims.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import tempfile
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from threading import RLock
30
+ from typing import Any
31
+
32
+ # v1 schema. Bump only on breaking changes; readers handle older versions.
33
+ SCHEMA_VERSION = 1
34
+
35
+
36
+ def _home_dir() -> Path:
37
+ """Resolve HYPERMIND_HOME/swarm_memory, defaulting to ~/.hypermind."""
38
+ env = os.environ.get("HYPERMIND_HOME")
39
+ base = Path(env).expanduser() if env else (Path.home() / ".hypermind")
40
+ return base / "swarm_memory"
41
+
42
+
43
+ @dataclass
44
+ class PersonaKernelState:
45
+ """A single persona's per-topic memory.
46
+
47
+ Persisted as a JSON object keyed by topic. Each topic carries the
48
+ last posterior probability (``post``), a running Brier mean, the
49
+ number of resolved oracle outcomes contributing, and ``last_ms``.
50
+ """
51
+
52
+ persona: str
53
+ posteriors: dict[str, float] = field(default_factory=dict)
54
+ brier_mean: dict[str, float] = field(default_factory=dict)
55
+ n_observations: dict[str, int] = field(default_factory=dict)
56
+ last_ms: int = 0
57
+ n_sims: int = 0
58
+
59
+ def to_dict(self) -> dict[str, Any]:
60
+ return {
61
+ "version": SCHEMA_VERSION,
62
+ "persona": self.persona,
63
+ "posteriors": dict(self.posteriors),
64
+ "brier_mean": dict(self.brier_mean),
65
+ "n_observations": dict(self.n_observations),
66
+ "last_ms": int(self.last_ms),
67
+ "n_sims": int(self.n_sims),
68
+ }
69
+
70
+ @classmethod
71
+ def from_dict(cls, d: dict[str, Any]) -> PersonaKernelState:
72
+ # Tolerate older versions: only refuse on completely unknown shapes.
73
+ v = int(d.get("version", 0) or 0)
74
+ if v > SCHEMA_VERSION:
75
+ # Future schema — refuse rather than silently downgrade.
76
+ raise ValueError(f"unknown KernelStore schema version {v}")
77
+ return cls(
78
+ persona=str(d.get("persona", "")),
79
+ posteriors={str(k): float(v) for k, v in (d.get("posteriors") or {}).items()},
80
+ brier_mean={str(k): float(v) for k, v in (d.get("brier_mean") or {}).items()},
81
+ n_observations={str(k): int(v) for k, v in (d.get("n_observations") or {}).items()},
82
+ last_ms=int(d.get("last_ms") or 0),
83
+ n_sims=int(d.get("n_sims") or 0),
84
+ )
85
+
86
+
87
+ class KernelStore:
88
+ """Threadsafe singleton: load/save persona kernel state per persona slug.
89
+
90
+ All reads/writes are atomic via tempfile + os.replace. The on-disk
91
+ location is ``HYPERMIND_HOME/swarm_memory/<persona>.json``. Tests
92
+ overriding ``HYPERMIND_HOME`` get full isolation for free.
93
+ """
94
+
95
+ _LOCK = RLock()
96
+
97
+ @staticmethod
98
+ def _path_for(persona: str) -> Path:
99
+ # Persona slugs come from a closed set in personas.py and pass a
100
+ # filename safety check anyway, but we still sanitise hard.
101
+ safe = "".join(ch for ch in persona if ch.isalnum() or ch in "-_") or "default"
102
+ return _home_dir() / f"{safe}.json"
103
+
104
+ @classmethod
105
+ def load(cls, persona: str) -> PersonaKernelState:
106
+ """Read this persona's state from disk. Returns empty state if no
107
+ prior file exists or if the file is unreadable."""
108
+ if not persona:
109
+ return PersonaKernelState(persona="")
110
+ p = cls._path_for(persona)
111
+ with cls._LOCK:
112
+ if not p.exists():
113
+ return PersonaKernelState(persona=persona)
114
+ try:
115
+ raw = json.loads(p.read_text())
116
+ state = PersonaKernelState.from_dict(raw)
117
+ if not state.persona:
118
+ state.persona = persona
119
+ return state
120
+ except (OSError, ValueError, json.JSONDecodeError):
121
+ # Corrupted file — log via stderr but never break the sim.
122
+ return PersonaKernelState(persona=persona)
123
+
124
+ @classmethod
125
+ def save(cls, state: PersonaKernelState) -> None:
126
+ """Atomically persist state. No-ops on empty persona name."""
127
+ if not state.persona:
128
+ return
129
+ p = cls._path_for(state.persona)
130
+ with cls._LOCK:
131
+ p.parent.mkdir(parents=True, exist_ok=True)
132
+ payload = json.dumps(state.to_dict(), indent=2, sort_keys=True)
133
+ # Atomic write so a crash mid-write never corrupts the file.
134
+ fd, tmp = tempfile.mkstemp(prefix=f".{p.name}.", dir=str(p.parent))
135
+ try:
136
+ with os.fdopen(fd, "w") as f:
137
+ f.write(payload)
138
+ os.replace(tmp, p)
139
+ except OSError:
140
+ try:
141
+ os.unlink(tmp)
142
+ except OSError:
143
+ pass
144
+ raise
145
+
146
+ @classmethod
147
+ def update(
148
+ cls,
149
+ persona: str,
150
+ topic: str,
151
+ *,
152
+ posterior: float | None = None,
153
+ brier_observation: float | None = None,
154
+ n_sims_increment: int = 0,
155
+ ) -> PersonaKernelState:
156
+ """Read-modify-write convenience. Returns the new state."""
157
+ state = cls.load(persona)
158
+ if posterior is not None:
159
+ state.posteriors[topic] = float(posterior)
160
+ if brier_observation is not None:
161
+ n = state.n_observations.get(topic, 0)
162
+ mean = state.brier_mean.get(topic, 0.0)
163
+ new_n = n + 1
164
+ new_mean = (mean * n + float(brier_observation)) / new_n
165
+ state.n_observations[topic] = new_n
166
+ state.brier_mean[topic] = new_mean
167
+ if n_sims_increment:
168
+ state.n_sims += int(n_sims_increment)
169
+ state.last_ms = int(time.time() * 1000)
170
+ cls.save(state)
171
+ return state
172
+
173
+ @classmethod
174
+ def lifetime_summary(cls, persona: str) -> dict[str, Any]:
175
+ """Read-only summary used by SimList / Home / Detail Lineage UIs."""
176
+ state = cls.load(persona)
177
+ n_topics = len(state.brier_mean)
178
+ if n_topics == 0:
179
+ mean_brier: float | None = None
180
+ else:
181
+ mean_brier = sum(state.brier_mean.values()) / n_topics
182
+ return {
183
+ "persona": state.persona,
184
+ "n_sims": state.n_sims,
185
+ "n_topics": n_topics,
186
+ "mean_brier": mean_brier,
187
+ "last_ms": state.last_ms,
188
+ "topics": dict(state.brier_mean),
189
+ }
190
+
191
+ @classmethod
192
+ def list_personas(cls) -> list[str]:
193
+ """Personas with persisted state in HYPERMIND_HOME."""
194
+ d = _home_dir()
195
+ if not d.exists():
196
+ return []
197
+ return sorted(p.stem for p in d.glob("*.json"))