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,523 @@
1
+ """SwarmMemory — CRDT-backed shared scratchpad for the collective.
2
+
3
+ Three sub-stores per spec §20.4:
4
+
5
+ * **consensus_state** — per-topic compressed posterior + last HLC.
6
+ * **lessons_learned** — append-only (claim_kid, oracle_outcome, rationale).
7
+ * **swarm_state_vector** — fixed-dim embedding summarising recent consensus.
8
+
9
+ CRDT semantics: last-write-wins-with-evidence. Every write carries the
10
+ issuer kid + HLC; reads are local. Conflicts resolve by HLC (then by
11
+ kid as a tiebreaker).
12
+
13
+ Wire-format-clean: when serialised for gossip, each record rides inside
14
+ a SEAL envelope as ``payload.swarm_record`` — no envelope-schema-hash
15
+ bump, no v1.0 freeze risk.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import logging
22
+ import warnings
23
+ from collections import deque
24
+ from collections.abc import Iterable
25
+ from dataclasses import dataclass
26
+
27
+ import cbor2
28
+
29
+ _log = logging.getLogger(__name__)
30
+
31
+ SWARM_RECORD_VERSION = 1
32
+ SWARM_VECTOR_MAX_DIM = 1024
33
+
34
+
35
+ class SwarmVectorOverflowWarning(UserWarning):
36
+ """Emitted when topic count exceeds SWARM_VECTOR_MAX_DIM and extra topics are truncated."""
37
+
38
+
39
+ # DoS gate: oversize CBOR payloads are rejected before parsing. Swarm
40
+ # records carry vectors up to SWARM_VECTOR_MAX_DIM floats; 1 MiB is a
41
+ # generous ceiling that still bounds parser-amplification attacks.
42
+ MAX_SWARM_RECORD_BYTES = 1024 * 1024
43
+ MAX_LESSONS = 10_000
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Records
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ConsensusEntry:
53
+ topic: str
54
+ posterior: float
55
+ n_contributors: int
56
+ last_hlc_ms: int
57
+ issuer_kid: bytes
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class LessonEntry:
62
+ claim_kid: bytes
63
+ topic: str
64
+ oracle_outcome: float
65
+ predicted_consensus: float
66
+ rationale: str
67
+ hlc_ms: int
68
+ issuer_kid: bytes
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class SwarmStateVector:
73
+ topic: str
74
+ vector: tuple[float, ...]
75
+ last_hlc_ms: int
76
+ issuer_kid: bytes
77
+
78
+ def __post_init__(self) -> None:
79
+ if len(self.vector) > SWARM_VECTOR_MAX_DIM:
80
+ raise ValueError(
81
+ f"swarm_state_vector dim {len(self.vector)} > max {SWARM_VECTOR_MAX_DIM}"
82
+ )
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Memory
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ class SwarmMemory:
91
+ """Per-namespace shared memory. In-process LWW CRDT for v0.9b.
92
+
93
+ T4-04 (cross-namespace federation): a SwarmMemory may :meth:`federate`
94
+ with a remote SwarmMemory through a named bridge_kid. Bridge-mediated
95
+ merges record ``provenance_namespace`` on every fork-log entry so
96
+ auditors can trace where a conflict originated.
97
+
98
+ Per the §20 invariant (also enforced by ``mind/federation.assert_never_merge``)
99
+ foreign reputation/state is NEVER silently merged into the home
100
+ kernel — it is exposed via :meth:`foreign_consensus` for display only.
101
+ """
102
+
103
+ def __init__(self, namespace_id: bytes | None = None) -> None:
104
+ self.namespace_id: bytes | None = bytes(namespace_id) if namespace_id else None
105
+ self._consensus: dict[str, ConsensusEntry] = {}
106
+ self._lessons: list[LessonEntry] = []
107
+ self._vectors: dict[str, SwarmStateVector] = {}
108
+ # Fork log: records (hlc, kid, winner_hash) for each conflict that
109
+ # was resolved by deterministic tiebreak rather than crashing.
110
+ # Bounded at 100 entries — older conflicts are evicted.
111
+ self.fork_log: deque[dict] = deque(maxlen=100)
112
+ # Cross-namespace bridges: remote_namespace_id_hex -> bridge_kid.
113
+ self._bridges: dict[str, bytes] = {}
114
+ # Foreign read-through cache populated on bridge merge; never
115
+ # writes into self._consensus to preserve never-merge invariant.
116
+ self._foreign_consensus: dict[tuple[str, str], ConsensusEntry] = {}
117
+
118
+ # ---- consensus_state ---------------------------------------------------
119
+
120
+ def upsert_consensus(self, entry: ConsensusEntry) -> bool:
121
+ """Apply LWW write. Returns True if accepted.
122
+
123
+ Tiebreak rules at identical HLC:
124
+ * smaller ``issuer_kid`` loses (lex compare, strict ``<``);
125
+ * equal ``issuer_kid`` requires byte-identical payload — a
126
+ divergent payload at the same (hlc, kid) is a CRDT protocol
127
+ violation and raises ``ValueError``. Same-(hlc, kid) with
128
+ identical payload is an idempotent no-op (returns True).
129
+ """
130
+ existing = self._consensus.get(entry.topic)
131
+ if existing is not None:
132
+ if entry.last_hlc_ms < existing.last_hlc_ms:
133
+ return False
134
+ if entry.last_hlc_ms == existing.last_hlc_ms:
135
+ if entry.issuer_kid == existing.issuer_kid:
136
+ if entry == existing:
137
+ return True
138
+ # Conflict: same (hlc, kid) with different payload.
139
+ # Log the fork and apply deterministic tiebreak: keep
140
+ # the entry whose CBOR encoding has the lexicographically
141
+ # smaller SHA-256 hash (stable, reproducible on every peer).
142
+ import cbor2 as _cbor2
143
+
144
+ h_existing = hashlib.sha256(
145
+ _cbor2.dumps(
146
+ existing.issuer_kid
147
+ + existing.last_hlc_ms.to_bytes(8, "big")
148
+ + str(existing.posterior).encode(),
149
+ canonical=True,
150
+ )
151
+ ).digest()
152
+ h_entry = hashlib.sha256(
153
+ _cbor2.dumps(
154
+ entry.issuer_kid
155
+ + entry.last_hlc_ms.to_bytes(8, "big")
156
+ + str(entry.posterior).encode(),
157
+ canonical=True,
158
+ )
159
+ ).digest()
160
+ self.fork_log.append(
161
+ {
162
+ "store": "consensus",
163
+ "topic": entry.topic,
164
+ "hlc": entry.last_hlc_ms,
165
+ "kid": entry.issuer_kid,
166
+ "winner_hash": min(h_existing, h_entry).hex(),
167
+ }
168
+ )
169
+ _log.warning(
170
+ "SwarmMemory fork: consensus (hlc=%s, kid=%s) has conflicting payload; "
171
+ "applying deterministic tiebreak",
172
+ entry.last_hlc_ms,
173
+ entry.issuer_kid[:4].hex(),
174
+ )
175
+ if h_entry < h_existing:
176
+ self._consensus[entry.topic] = entry
177
+ return True
178
+ return False
179
+ if entry.issuer_kid < existing.issuer_kid:
180
+ return False
181
+ self._consensus[entry.topic] = entry
182
+ return True
183
+
184
+ def consensus(self, topic: str) -> ConsensusEntry | None:
185
+ return self._consensus.get(topic)
186
+
187
+ def all_consensus(self) -> list[ConsensusEntry]:
188
+ return sorted(self._consensus.values(), key=lambda c: c.topic)
189
+
190
+ # ---- lessons_learned ---------------------------------------------------
191
+
192
+ def append_lesson(self, lesson: LessonEntry) -> None:
193
+ self._lessons.append(lesson)
194
+ if len(self._lessons) > MAX_LESSONS:
195
+ self._lessons = self._lessons[-MAX_LESSONS:]
196
+
197
+ def lessons_for(self, topic: str | None = None) -> list[LessonEntry]:
198
+ if topic is None:
199
+ return list(self._lessons)
200
+ return [lesson for lesson in self._lessons if lesson.topic == topic]
201
+
202
+ def lesson_count(self) -> int:
203
+ return len(self._lessons)
204
+
205
+ # ---- swarm_state_vector ------------------------------------------------
206
+
207
+ def upsert_vector(self, vec: SwarmStateVector) -> bool:
208
+ existing = self._vectors.get(vec.topic)
209
+ if existing is not None:
210
+ if vec.last_hlc_ms < existing.last_hlc_ms:
211
+ return False
212
+ if vec.last_hlc_ms == existing.last_hlc_ms:
213
+ if vec.issuer_kid == existing.issuer_kid:
214
+ if vec == existing:
215
+ return True
216
+ # Conflict: same (hlc, kid) with different payload.
217
+ import cbor2 as _cbor2
218
+
219
+ h_existing = hashlib.sha256(
220
+ _cbor2.dumps(
221
+ existing.issuer_kid
222
+ + existing.last_hlc_ms.to_bytes(8, "big")
223
+ + str(existing.vector).encode(),
224
+ canonical=True,
225
+ )
226
+ ).digest()
227
+ h_vec = hashlib.sha256(
228
+ _cbor2.dumps(
229
+ vec.issuer_kid
230
+ + vec.last_hlc_ms.to_bytes(8, "big")
231
+ + str(vec.vector).encode(),
232
+ canonical=True,
233
+ )
234
+ ).digest()
235
+ self.fork_log.append(
236
+ {
237
+ "store": "vectors",
238
+ "topic": vec.topic,
239
+ "hlc": vec.last_hlc_ms,
240
+ "kid": vec.issuer_kid,
241
+ "winner_hash": min(h_existing, h_vec).hex(),
242
+ }
243
+ )
244
+ _log.warning(
245
+ "SwarmMemory fork: vector (hlc=%s, kid=%s) has conflicting payload; "
246
+ "applying deterministic tiebreak",
247
+ vec.last_hlc_ms,
248
+ vec.issuer_kid[:4].hex(),
249
+ )
250
+ if h_vec < h_existing:
251
+ self._vectors[vec.topic] = vec
252
+ return True
253
+ return False
254
+ if vec.issuer_kid < existing.issuer_kid:
255
+ return False
256
+ self._vectors[vec.topic] = vec
257
+ return True
258
+
259
+ def vector(self, topic: str) -> SwarmStateVector | None:
260
+ return self._vectors.get(topic)
261
+
262
+ # ---- bulk ops ----------------------------------------------------------
263
+
264
+ # ---- T4-04: cross-namespace federation --------------------------------
265
+
266
+ def federate(self, remote_namespace_id: bytes, bridge_kid: bytes) -> None:
267
+ """Establish a bridge to another namespace's SwarmMemory.
268
+
269
+ ``bridge_kid`` is the kid of the bridge agent authorised to gossip
270
+ across the boundary. Only one bridge per remote namespace; calling
271
+ :meth:`federate` again replaces the existing bridge (rotation).
272
+ """
273
+ ns_hex = bytes(remote_namespace_id).hex()
274
+ self._bridges[ns_hex] = bytes(bridge_kid)
275
+
276
+ def bridges(self) -> dict[str, bytes]:
277
+ return dict(self._bridges)
278
+
279
+ def merge_from_bridge(
280
+ self,
281
+ other: SwarmMemory,
282
+ *,
283
+ remote_namespace_id: bytes,
284
+ bridge_kid: bytes,
285
+ ) -> dict[str, int]:
286
+ """Bridge-mediated cross-namespace merge.
287
+
288
+ Foreign consensus entries are written into a *separate*
289
+ ``_foreign_consensus`` map (read-only via :meth:`foreign_consensus`)
290
+ so the home reputation/state remains untouched. Conflicts produce
291
+ fork-log entries enriched with ``provenance_namespace``.
292
+ """
293
+ ns_hex = bytes(remote_namespace_id).hex()
294
+ if self._bridges.get(ns_hex) != bytes(bridge_kid):
295
+ raise ValueError(
296
+ f"merge_from_bridge: bridge_kid does not match registered "
297
+ f"bridge for namespace {ns_hex[:16]}…"
298
+ )
299
+ applied = {"foreign_consensus": 0, "lessons": 0}
300
+ # Foreign consensus: never merged into home; always displayed.
301
+ for c in other.all_consensus():
302
+ key = (ns_hex, c.topic)
303
+ existing = self._foreign_consensus.get(key)
304
+ if existing is None or existing.last_hlc_ms < c.last_hlc_ms:
305
+ self._foreign_consensus[key] = c
306
+ applied["foreign_consensus"] += 1
307
+ elif existing.last_hlc_ms == c.last_hlc_ms and existing != c:
308
+ # Cross-namespace conflict at identical HLC — fork-log it.
309
+ self.fork_log.append(
310
+ {
311
+ "store": "foreign_consensus",
312
+ "topic": c.topic,
313
+ "hlc": c.last_hlc_ms,
314
+ "kid": c.issuer_kid,
315
+ "provenance_namespace": ns_hex,
316
+ "reason": "CROSS-NS-CONFLICT",
317
+ }
318
+ )
319
+ # Lessons may cross — they are append-only and globally addressed
320
+ # by claim_kid; no risk of silent reputation merge.
321
+ existing_kids = {lesson.claim_kid for lesson in self._lessons}
322
+ for lesson in other.lessons_for():
323
+ if lesson.claim_kid not in existing_kids:
324
+ self._lessons.append(lesson)
325
+ existing_kids.add(lesson.claim_kid)
326
+ applied["lessons"] += 1
327
+ return applied
328
+
329
+ def foreign_consensus(
330
+ self, *, remote_namespace_id: bytes | None = None, topic: str | None = None
331
+ ) -> list[tuple[str, ConsensusEntry]]:
332
+ """Return foreign consensus entries; ``[(ns_hex, entry), …]``.
333
+
334
+ Filtered by namespace and/or topic when provided. This is the
335
+ ONLY read path for foreign state — it is intentionally separate
336
+ from :meth:`consensus` to make the never-merge invariant
337
+ structural rather than aspirational.
338
+ """
339
+ ns_filter = bytes(remote_namespace_id).hex() if remote_namespace_id else None
340
+ out: list[tuple[str, ConsensusEntry]] = []
341
+ for (ns_hex, tpc), entry in self._foreign_consensus.items():
342
+ if ns_filter is not None and ns_hex != ns_filter:
343
+ continue
344
+ if topic is not None and tpc != topic:
345
+ continue
346
+ out.append((ns_hex, entry))
347
+ return out
348
+
349
+ def merge(self, other: SwarmMemory) -> dict[str, int]:
350
+ """LWW-merge another memory's records. Returns counters per sub-store.
351
+
352
+ Counts only *state-changing* applications — an idempotent
353
+ re-application of a byte-identical entry at the same (hlc, kid)
354
+ does not increment the counter.
355
+ """
356
+ applied = {"consensus": 0, "lessons": 0, "vectors": 0}
357
+ for c in other.all_consensus():
358
+ before = self._consensus.get(c.topic)
359
+ if self.upsert_consensus(c) and self._consensus.get(c.topic) is not before:
360
+ applied["consensus"] += 1
361
+ # Lessons are append-only with idempotent dedup by claim_kid only.
362
+ # claim_kid is a 32-byte SHA-256 hash, globally unique per claim per spec,
363
+ # so deduping by claim_kid alone correctly drops re-gossip duplicates while
364
+ # still accepting valid lessons from different issuers on the same topic.
365
+ existing = {lesson.claim_kid for lesson in self._lessons}
366
+ for lesson in other.lessons_for():
367
+ if lesson.claim_kid not in existing:
368
+ self._lessons.append(lesson)
369
+ existing.add(lesson.claim_kid)
370
+ applied["lessons"] += 1
371
+ for v in other._vectors.values():
372
+ before_v = self._vectors.get(v.topic)
373
+ if self.upsert_vector(v) and self._vectors.get(v.topic) is not before_v:
374
+ applied["vectors"] += 1
375
+ return applied
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # Wire-clean encoding (rides inside SEAL payload)
380
+ # ---------------------------------------------------------------------------
381
+
382
+ _KEY_KIND = 0
383
+ _KEY_BODY = 1
384
+ _KEY_VERSION = 2
385
+
386
+
387
+ def encode_swarm_record(
388
+ record: ConsensusEntry | LessonEntry | SwarmStateVector,
389
+ ) -> bytes:
390
+ if isinstance(record, ConsensusEntry):
391
+ body = {
392
+ "topic": record.topic,
393
+ "posterior": record.posterior,
394
+ "n_contributors": record.n_contributors,
395
+ "last_hlc_ms": record.last_hlc_ms,
396
+ "issuer_kid": record.issuer_kid,
397
+ }
398
+ kind = "consensus"
399
+ elif isinstance(record, LessonEntry):
400
+ body = {
401
+ "claim_kid": record.claim_kid,
402
+ "topic": record.topic,
403
+ "oracle_outcome": record.oracle_outcome,
404
+ "predicted_consensus": record.predicted_consensus,
405
+ "rationale": record.rationale,
406
+ "hlc_ms": record.hlc_ms,
407
+ "issuer_kid": record.issuer_kid,
408
+ }
409
+ kind = "lesson"
410
+ elif isinstance(record, SwarmStateVector):
411
+ body = {
412
+ "topic": record.topic,
413
+ "vector": list(record.vector),
414
+ "last_hlc_ms": record.last_hlc_ms,
415
+ "issuer_kid": record.issuer_kid,
416
+ }
417
+ kind = "vector"
418
+ else:
419
+ raise TypeError(f"unknown swarm record: {type(record).__name__}")
420
+ return cbor2.dumps(
421
+ {_KEY_KIND: kind, _KEY_BODY: body, _KEY_VERSION: SWARM_RECORD_VERSION},
422
+ canonical=True,
423
+ )
424
+
425
+
426
+ def decode_swarm_record(
427
+ raw: bytes,
428
+ *,
429
+ expected_signer_kid: bytes,
430
+ ) -> ConsensusEntry | LessonEntry | SwarmStateVector:
431
+ """Decode a swarm record envelope.
432
+
433
+ The ``expected_signer_kid`` is the kid of the outer SignedStatement
434
+ signer; it MUST match the embedded ``issuer_kid`` in the record body
435
+ to prevent a kid spoofing attack where a valid signer wraps a payload
436
+ that falsely attributes authorship to another agent.
437
+
438
+ Raises ``ValueError`` on mismatch, oversize input, or malformed CBOR.
439
+ """
440
+ if len(raw) > MAX_SWARM_RECORD_BYTES:
441
+ raise ValueError(f"swarm record too large: {len(raw)} > {MAX_SWARM_RECORD_BYTES}")
442
+ try:
443
+ env = cbor2.loads(raw)
444
+ except Exception as e:
445
+ raise ValueError(f"swarm record decode: not CBOR: {e}") from e
446
+ if not isinstance(env, dict):
447
+ raise ValueError("swarm record decode: not a map")
448
+ if env.get(_KEY_VERSION) != SWARM_RECORD_VERSION:
449
+ raise ValueError(f"swarm record version: {env.get(_KEY_VERSION)!r}")
450
+ kind = env.get(_KEY_KIND)
451
+ body = env.get(_KEY_BODY) or {}
452
+ embedded_kid = bytes(body.get("issuer_kid", b""))
453
+ if embedded_kid != bytes(expected_signer_kid):
454
+ raise ValueError(
455
+ "issuer_kid mismatch: payload claims "
456
+ f"{embedded_kid[:8].hex()}, signer is "
457
+ f"{bytes(expected_signer_kid)[:8].hex()}"
458
+ )
459
+ if kind == "consensus":
460
+ return ConsensusEntry(
461
+ topic=str(body["topic"]),
462
+ posterior=float(body["posterior"]),
463
+ n_contributors=int(body["n_contributors"]),
464
+ last_hlc_ms=int(body["last_hlc_ms"]),
465
+ issuer_kid=embedded_kid,
466
+ )
467
+ if kind == "lesson":
468
+ return LessonEntry(
469
+ claim_kid=bytes(body["claim_kid"]),
470
+ topic=str(body["topic"]),
471
+ oracle_outcome=float(body["oracle_outcome"]),
472
+ predicted_consensus=float(body["predicted_consensus"]),
473
+ rationale=str(body.get("rationale", "")),
474
+ hlc_ms=int(body["hlc_ms"]),
475
+ issuer_kid=embedded_kid,
476
+ )
477
+ if kind == "vector":
478
+ return SwarmStateVector(
479
+ topic=str(body["topic"]),
480
+ vector=tuple(float(x) for x in body["vector"]),
481
+ last_hlc_ms=int(body["last_hlc_ms"]),
482
+ issuer_kid=embedded_kid,
483
+ )
484
+ raise ValueError(f"swarm record decode: unknown kind {kind!r}")
485
+
486
+
487
+ # ---------------------------------------------------------------------------
488
+ # Embedding helper
489
+ # ---------------------------------------------------------------------------
490
+
491
+
492
+ def compress_consensus_to_vector(
493
+ entries: Iterable[ConsensusEntry],
494
+ *,
495
+ dim: int = 32,
496
+ ) -> tuple[float, ...]:
497
+ """Project a set of per-topic posteriors into a fixed-dim vector.
498
+
499
+ Stable ordering by topic name; unused dimensions zero-padded.
500
+ Bounded at SWARM_VECTOR_MAX_DIM. Pure deterministic.
501
+
502
+ When the number of topics exceeds ``dim`` (or when ``dim`` equals
503
+ ``SWARM_VECTOR_MAX_DIM`` and the topic count exceeds it), a
504
+ :class:`SwarmVectorOverflowWarning` is emitted so callers are aware
505
+ that extra topics are silently truncated.
506
+ """
507
+ if dim > SWARM_VECTOR_MAX_DIM:
508
+ raise ValueError(f"dim {dim} > SWARM_VECTOR_MAX_DIM {SWARM_VECTOR_MAX_DIM}")
509
+ rows = sorted(entries, key=lambda e: e.topic)
510
+ n = len(rows)
511
+ if n > dim:
512
+ warnings.warn(
513
+ f"SwarmMemory: topic count {n} exceeds SWARM_VECTOR_MAX_DIM={SWARM_VECTOR_MAX_DIM};"
514
+ " extra topics truncated",
515
+ SwarmVectorOverflowWarning,
516
+ stacklevel=2,
517
+ )
518
+ out = [0.0] * dim
519
+ for i, e in enumerate(rows):
520
+ if i >= dim:
521
+ break
522
+ out[i] = float(e.posterior)
523
+ return tuple(out)