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,438 @@
1
+ """Distributed swarm-trace — W3C TraceContext propagation across SignedStatements.
2
+
3
+ T6-01 (v0.10.4). The transparency log is the canonical timeline (spec §28.4),
4
+ but operators also need OpenTelemetry-shaped span trees that span multiple
5
+ hosts so a Grafana / Jaeger view can stitch a deliberation, a saga workflow
6
+ and a contract-net RFP into one trace.
7
+
8
+ Design:
9
+
10
+ * Each :class:`SwarmTrace` carries a 16-byte ``trace_id`` (W3C). Every
11
+ :meth:`start_span` mints a fresh 8-byte ``span_id`` and links it to the
12
+ parent ``span_id`` (if any). The triple ``(trace_id, span_id, flags)`` —
13
+ optionally followed by an 8-byte parent_span_id — is encoded as a single
14
+ bytes blob and placed under the protected-header label
15
+ :data:`hypermind.wire.HM_OTEL_TRACE_CONTEXT` (= -65635) by
16
+ :meth:`inject`. :meth:`extract` is the inverse.
17
+
18
+ * Because the label is in the protected header, A-ALG-BOUND covers it: a
19
+ tampered trace context fails canonical re-encoding before signature
20
+ verification.
21
+
22
+ * :meth:`replay_from_log` rebuilds a span tree post-hoc by reading the
23
+ per-namespace transparency log. It does **not** require the trace
24
+ context label to be present (older envelopes simply contribute no
25
+ span); HLC order is the secondary discriminator that produces a stable
26
+ total order even when wall-clock skew is large.
27
+
28
+ The class is deliberately small: it has zero hard dependency on the
29
+ ``opentelemetry`` SDK — emit live spans by chaining a regular
30
+ ``Recorder.with_otel(tracer_provider)`` after :meth:`start_span`.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import os
36
+ from dataclasses import dataclass, field
37
+ from typing import Any
38
+
39
+ # Wire-format label. Imported lazily-via-module so this file stays cheap to
40
+ # import even when the wire module isn't otherwise needed.
41
+ from hypermind.wire import HM_OTEL_TRACE_CONTEXT
42
+
43
+ #: Length of a W3C trace_id in bytes (RFC: 16 bytes / 32 hex chars).
44
+ TRACE_ID_LEN = 16
45
+ #: Length of a W3C span_id in bytes (RFC: 8 bytes / 16 hex chars).
46
+ SPAN_ID_LEN = 8
47
+ #: Length of the flags byte (RFC: 1 byte).
48
+ FLAGS_LEN = 1
49
+ #: Length of an injected blob without the parent_span_id (16 + 8 + 1).
50
+ TRACE_CONTEXT_LEN = TRACE_ID_LEN + SPAN_ID_LEN + FLAGS_LEN
51
+ #: Length when the optional parent_span_id is appended.
52
+ TRACE_CONTEXT_LEN_WITH_PARENT = TRACE_CONTEXT_LEN + SPAN_ID_LEN
53
+
54
+ #: Sampled flag (W3C TraceContext §2.2.4).
55
+ TRACE_FLAG_SAMPLED = 0x01
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class SpanCtx:
60
+ """Immutable snapshot of an active span.
61
+
62
+ The active span is the parent for any subsequent :meth:`SwarmTrace.start_span`
63
+ call until the SpanCtx is finished. This dataclass is wire-portable —
64
+ callers pass it across coroutine / process boundaries by serialising
65
+ via :meth:`SwarmTrace.inject`.
66
+ """
67
+
68
+ trace_id: bytes
69
+ span_id: bytes
70
+ parent_span_id: bytes | None
71
+ name: str
72
+ flags: int = TRACE_FLAG_SAMPLED
73
+ attributes: dict[str, Any] = field(default_factory=dict)
74
+ start_hlc: str | None = None
75
+
76
+ def __post_init__(self) -> None:
77
+ if len(self.trace_id) != TRACE_ID_LEN:
78
+ raise ValueError(f"trace_id must be {TRACE_ID_LEN} bytes, got {len(self.trace_id)}")
79
+ if len(self.span_id) != SPAN_ID_LEN:
80
+ raise ValueError(f"span_id must be {SPAN_ID_LEN} bytes, got {len(self.span_id)}")
81
+ if self.parent_span_id is not None and len(self.parent_span_id) != SPAN_ID_LEN:
82
+ raise ValueError(
83
+ f"parent_span_id must be {SPAN_ID_LEN} bytes or None, "
84
+ f"got {len(self.parent_span_id)}"
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class Span:
90
+ """Materialised span used by :meth:`SwarmTrace.replay_from_log`."""
91
+
92
+ trace_id: bytes
93
+ span_id: bytes
94
+ parent_span_id: bytes | None
95
+ name: str
96
+ hlc: str
97
+ kid: bytes | None = None
98
+ attributes: dict[str, Any] = field(default_factory=dict)
99
+
100
+
101
+ class SwarmTrace:
102
+ """W3C TraceContext propagator for SignedStatement envelopes.
103
+
104
+ Usage::
105
+
106
+ tracer = SwarmTrace()
107
+ ctx = tracer.start_span("deliberate")
108
+ envelope = build_envelope(...)
109
+ envelope_with_ctx = tracer.inject(envelope, ctx)
110
+ # ... ship envelope across the bus ...
111
+ peer_ctx = tracer.extract(received_envelope) # may be None
112
+ child = tracer.start_span("vote", parent_context=peer_ctx)
113
+ """
114
+
115
+ def __init__(self, *, trace_id: bytes | None = None) -> None:
116
+ self._trace_id: bytes = trace_id if trace_id is not None else self._fresh_trace_id()
117
+ self._active: SpanCtx | None = None
118
+
119
+ # ---- public API ---------------------------------------------------
120
+
121
+ @property
122
+ def trace_id(self) -> bytes:
123
+ """The 16-byte trace_id all spans on this tracer share."""
124
+ return self._trace_id
125
+
126
+ @property
127
+ def active_span(self) -> SpanCtx | None:
128
+ """The most recent span started, or ``None``."""
129
+ return self._active
130
+
131
+ def start_span(
132
+ self,
133
+ name: str,
134
+ *,
135
+ parent_context: SpanCtx | None = None,
136
+ flags: int = TRACE_FLAG_SAMPLED,
137
+ attributes: dict[str, Any] | None = None,
138
+ start_hlc: str | None = None,
139
+ ) -> SpanCtx:
140
+ """Mint a fresh span. ``parent_context`` defaults to the active span."""
141
+ parent = parent_context if parent_context is not None else self._active
142
+ # If we cross into a foreign trace via parent_context, follow it.
143
+ trace_id = parent.trace_id if parent is not None else self._trace_id
144
+ span_id = self._fresh_span_id()
145
+ ctx = SpanCtx(
146
+ trace_id=trace_id,
147
+ span_id=span_id,
148
+ parent_span_id=parent.span_id if parent is not None else None,
149
+ name=name,
150
+ flags=flags,
151
+ attributes=dict(attributes or {}),
152
+ start_hlc=start_hlc,
153
+ )
154
+ self._active = ctx
155
+ return ctx
156
+
157
+ def inject(self, envelope: Any, ctx: SpanCtx | None = None) -> Any:
158
+ """Place the trace context into ``envelope.protected_header``.
159
+
160
+ ``envelope`` is duck-typed: any object exposing a mutable
161
+ ``protected_header`` mapping or a ``protected_header`` attribute
162
+ on a frozen dataclass works (we re-build via ``dataclasses.replace``
163
+ when needed). For raw dicts, the dict is mutated in place.
164
+ """
165
+ ctx = ctx if ctx is not None else self._active
166
+ if ctx is None:
167
+ return envelope
168
+ blob = encode_trace_context(ctx)
169
+ return _set_protected_header(envelope, HM_OTEL_TRACE_CONTEXT, blob)
170
+
171
+ def extract(self, envelope: Any) -> SpanCtx | None:
172
+ """Inverse of :meth:`inject`. Returns ``None`` when no context is present."""
173
+ blob = _get_protected_header(envelope, HM_OTEL_TRACE_CONTEXT)
174
+ if blob is None:
175
+ return None
176
+ try:
177
+ return decode_trace_context(blob, name="<extracted>")
178
+ except ValueError:
179
+ return None
180
+
181
+ def replay_from_log(self, transparency_store: Any) -> list[Span]:
182
+ """Reconstruct the span tree from a transparency log.
183
+
184
+ ``transparency_store`` is any object exposing one of:
185
+
186
+ * ``iter_envelopes()`` → iterable of decoded ``SignedStatement``
187
+ * ``envelopes`` attribute (list)
188
+ * ``leaves`` mapping ``leaf_kid → SignedStatement``
189
+
190
+ The output is sorted by HLC (lexical compare on the canonical
191
+ ``"<logical_ms>:<counter>:<kid>"`` form) so the parent → child
192
+ edges form a DAG even when wall-clock skew is large.
193
+ """
194
+ envelopes = list(_iter_envelopes(transparency_store))
195
+ envelopes.sort(key=lambda e: _envelope_hlc(e))
196
+ spans: list[Span] = []
197
+ for env in envelopes:
198
+ blob = _get_protected_header(env, HM_OTEL_TRACE_CONTEXT)
199
+ if blob is None:
200
+ continue
201
+ try:
202
+ ctx = decode_trace_context(blob, name=_envelope_record_type(env))
203
+ except ValueError:
204
+ continue
205
+ spans.append(
206
+ Span(
207
+ trace_id=ctx.trace_id,
208
+ span_id=ctx.span_id,
209
+ parent_span_id=ctx.parent_span_id,
210
+ name=ctx.name,
211
+ hlc=_envelope_hlc(env),
212
+ kid=_envelope_kid(env),
213
+ attributes=dict(ctx.attributes),
214
+ )
215
+ )
216
+ return spans
217
+
218
+ # ---- helpers ------------------------------------------------------
219
+
220
+ @staticmethod
221
+ def _fresh_trace_id() -> bytes:
222
+ return os.urandom(TRACE_ID_LEN)
223
+
224
+ @staticmethod
225
+ def _fresh_span_id() -> bytes:
226
+ return os.urandom(SPAN_ID_LEN)
227
+
228
+
229
+ # ---- wire encoding -------------------------------------------------------
230
+
231
+
232
+ def encode_trace_context(ctx: SpanCtx) -> bytes:
233
+ """Pack a SpanCtx into the wire blob format.
234
+
235
+ Layout:
236
+ trace_id(16) || span_id(8) || flags(1) [|| parent_span_id(8)]
237
+
238
+ The parent_span_id is included only when present so root spans get
239
+ the shorter 25-byte form.
240
+ """
241
+ base = ctx.trace_id + ctx.span_id + bytes([ctx.flags & 0xFF])
242
+ if ctx.parent_span_id is None:
243
+ return base
244
+ return base + ctx.parent_span_id
245
+
246
+
247
+ def decode_trace_context(blob: bytes, *, name: str = "") -> SpanCtx:
248
+ """Parse a trace context blob. Raises ``ValueError`` on malformed input."""
249
+ if len(blob) not in (TRACE_CONTEXT_LEN, TRACE_CONTEXT_LEN_WITH_PARENT):
250
+ raise ValueError(
251
+ f"trace context blob must be {TRACE_CONTEXT_LEN} or "
252
+ f"{TRACE_CONTEXT_LEN_WITH_PARENT} bytes, got {len(blob)}"
253
+ )
254
+ trace_id = blob[:TRACE_ID_LEN]
255
+ span_id = blob[TRACE_ID_LEN : TRACE_ID_LEN + SPAN_ID_LEN]
256
+ flags = blob[TRACE_ID_LEN + SPAN_ID_LEN]
257
+ parent: bytes | None = None
258
+ if len(blob) == TRACE_CONTEXT_LEN_WITH_PARENT:
259
+ parent = blob[TRACE_CONTEXT_LEN:]
260
+ return SpanCtx(
261
+ trace_id=trace_id,
262
+ span_id=span_id,
263
+ parent_span_id=parent,
264
+ name=name,
265
+ flags=flags,
266
+ )
267
+
268
+
269
+ # ---- duck-typing helpers -------------------------------------------------
270
+
271
+
272
+ def _set_protected_header(envelope: Any, label: int, value: bytes) -> Any:
273
+ ph = getattr(envelope, "protected_header", None)
274
+ if ph is None and isinstance(envelope, dict):
275
+ ph = envelope.setdefault("protected_header", {})
276
+ if ph is None:
277
+ # Last resort: stash on a plain attribute so tests can read it back.
278
+ try:
279
+ object.__setattr__(envelope, "_otel_trace_blob", value)
280
+ except (TypeError, AttributeError):
281
+ pass
282
+ return envelope
283
+ if isinstance(ph, dict):
284
+ ph[label] = value
285
+ else:
286
+ # Frozen dataclass / object with a mapping-shaped property.
287
+ try:
288
+ ph[label] = value # type: ignore[index]
289
+ except Exception as e: # pragma: no cover - defensive
290
+ raise TypeError(
291
+ f"cannot inject trace context into protected_header of type {type(ph)!r}: {e}"
292
+ ) from e
293
+ return envelope
294
+
295
+
296
+ def _get_protected_header(envelope: Any, label: int) -> bytes | None:
297
+ ph = getattr(envelope, "protected_header", None)
298
+ if ph is None and isinstance(envelope, dict):
299
+ ph = envelope.get("protected_header")
300
+ if ph is None:
301
+ # Match _set_protected_header's last-resort path.
302
+ return getattr(envelope, "_otel_trace_blob", None)
303
+ if isinstance(ph, dict):
304
+ v = ph.get(label)
305
+ else:
306
+ try:
307
+ v = ph[label] # type: ignore[index]
308
+ except (KeyError, TypeError, IndexError):
309
+ v = None
310
+ if v is None or isinstance(v, (bytes, bytearray)):
311
+ return bytes(v) if v is not None else None
312
+ return None
313
+
314
+
315
+ def _iter_envelopes(store: Any):
316
+ if hasattr(store, "iter_envelopes"):
317
+ yield from store.iter_envelopes()
318
+ return
319
+ envs = getattr(store, "envelopes", None)
320
+ if envs is not None:
321
+ yield from envs
322
+ return
323
+ leaves = getattr(store, "leaves", None)
324
+ if isinstance(leaves, dict):
325
+ yield from leaves.values()
326
+ return
327
+ if isinstance(store, list):
328
+ yield from store
329
+ return
330
+ raise TypeError(
331
+ f"transparency_store of type {type(store)!r} does not expose "
332
+ "iter_envelopes() / envelopes / leaves"
333
+ )
334
+
335
+
336
+ def _envelope_hlc(env: Any) -> str:
337
+ ph = getattr(env, "protected_header", None) or {}
338
+ if isinstance(env, dict):
339
+ ph = env.get("protected_header") or {}
340
+ # HM_HLC = -65603
341
+ hlc = ph.get(-65603) if isinstance(ph, dict) else None
342
+ if hlc is None:
343
+ hlc = getattr(env, "hlc", None)
344
+ return str(hlc) if hlc is not None else ""
345
+
346
+
347
+ def _envelope_kid(env: Any) -> bytes | None:
348
+ kid = getattr(env, "kid", None)
349
+ if kid is None and isinstance(env, dict):
350
+ kid = env.get("kid")
351
+ return kid if isinstance(kid, (bytes, bytearray)) else None
352
+
353
+
354
+ def _envelope_record_type(env: Any) -> str:
355
+ ph = getattr(env, "protected_header", None) or {}
356
+ if isinstance(env, dict):
357
+ ph = env.get("protected_header") or {}
358
+ rt = ph.get(-65601) if isinstance(ph, dict) else None
359
+ return str(rt) if rt is not None else "<span>"
360
+
361
+
362
+ # ---------------------------------------------------------------------------
363
+ # Normative-trace helpers (rows 37-40)
364
+ # ---------------------------------------------------------------------------
365
+
366
+ #: COSE protected-header label for the OTel trace context (spec §4.13.2).
367
+ #: Mirrors :data:`hypermind.wire.HM_OTEL_TRACE_CONTEXT` = -65635.
368
+ OTEL_TRACE_CONTEXT_LABEL: int = -65635
369
+
370
+
371
+ def new_span_id() -> bytes:
372
+ """Mint a fresh 8-byte ``span_id`` that is never all-zero (spec §28.3.2).
373
+
374
+ Retries on the astronomically unlikely all-zero draw, satisfying
375
+ the spec requirement that ``span_id != b'\\x00' * 8`` at every new
376
+ operation boundary.
377
+
378
+ Returns
379
+ -------
380
+ bytes
381
+ 8 cryptographically random bytes, guaranteed non-zero.
382
+ """
383
+ while True:
384
+ candidate = os.urandom(SPAN_ID_LEN)
385
+ if candidate != b"\x00" * SPAN_ID_LEN:
386
+ return candidate
387
+
388
+
389
+ def encode_trace_context_list(trace_id: bytes, span_id: bytes, flags: int) -> list:
390
+ """Encode the OTel trace context as a CBOR-encodable list.
391
+
392
+ Returns ``[trace_id, span_id, flags]`` — the canonical encoding
393
+ described in spec §4.13.2 and row 37 of the normative trace::
394
+
395
+ OTEL_TRACE_CONTEXT (-65635) = [bstr .size 16, bstr .size 8, uint]
396
+
397
+ This form is used when the protected header carries the triple as a
398
+ structured CBOR array rather than the concatenated-bytes blob used by
399
+ :func:`encode_trace_context` (which targets the existing ``SwarmTrace``
400
+ inject/extract path).
401
+
402
+ Parameters
403
+ ----------
404
+ trace_id:
405
+ 16-byte W3C trace identifier.
406
+ span_id:
407
+ 8-byte W3C span identifier.
408
+ flags:
409
+ Integer flags byte (e.g. ``TRACE_FLAG_SAMPLED = 0x01``).
410
+
411
+ Raises
412
+ ------
413
+ ValueError
414
+ If ``trace_id`` is not 16 bytes or ``span_id`` is not 8 bytes.
415
+ """
416
+ if len(trace_id) != TRACE_ID_LEN:
417
+ raise ValueError(f"trace_id must be {TRACE_ID_LEN} bytes, got {len(trace_id)}")
418
+ if len(span_id) != SPAN_ID_LEN:
419
+ raise ValueError(f"span_id must be {SPAN_ID_LEN} bytes, got {len(span_id)}")
420
+ return [trace_id, span_id, flags]
421
+
422
+
423
+ __all__ = [
424
+ "FLAGS_LEN",
425
+ "OTEL_TRACE_CONTEXT_LABEL",
426
+ "SPAN_ID_LEN",
427
+ "TRACE_CONTEXT_LEN",
428
+ "TRACE_CONTEXT_LEN_WITH_PARENT",
429
+ "TRACE_FLAG_SAMPLED",
430
+ "TRACE_ID_LEN",
431
+ "Span",
432
+ "SpanCtx",
433
+ "SwarmTrace",
434
+ "decode_trace_context",
435
+ "encode_trace_context",
436
+ "encode_trace_context_list",
437
+ "new_span_id",
438
+ ]
@@ -0,0 +1,116 @@
1
+ """PinPolicy DSL — declarative replication contract.
2
+
3
+ A pinner receives a stream of sealed claims via gossipsub and decides
4
+ whether to retain each one based on:
5
+ - topic match
6
+ - publisher reputation ≥ threshold
7
+ - claim_type filter
8
+ - replication target (e.g. "≥3 geo-disjoint, TTL=180d")
9
+ - erasure coding scheme
10
+ - disk budget + eviction policy
11
+
12
+ The DSL is a Python dataclass so validation, composition, and IDE
13
+ autocomplete all work at construction time. Late-bind YAML loaders are
14
+ non-normative sugar.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from typing import Literal
22
+
23
+ EvictionPolicy = Literal["lru", "reputation_weighted", "fifo"]
24
+
25
+
26
+ @dataclass
27
+ class PinPolicy:
28
+ """Declarative contract for what a pinner replicates."""
29
+
30
+ topics: list[str] = field(default_factory=list)
31
+ publishers_min_rep: float = 0.0
32
+ claim_types: list[str] | Literal["*"] = "*"
33
+ replication_target: tuple[str, str] = ("≥3 geo-disjoint", "TTL=180d")
34
+ erasure_coding: tuple[str, str] | None = None
35
+ max_disk_gb: float = 50.0
36
+ eviction: EvictionPolicy = "reputation_weighted"
37
+ cite_depth: int = 2
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.publishers_min_rep < 0 or self.publishers_min_rep > 1:
41
+ raise ValueError("publishers_min_rep must be in [0, 1]")
42
+ if self.max_disk_gb < 0:
43
+ raise ValueError("max_disk_gb must be non-negative")
44
+ if self.cite_depth < 0:
45
+ raise ValueError("cite_depth must be non-negative")
46
+
47
+ # Validate replication_target shape.
48
+ if not isinstance(self.replication_target, tuple) or len(self.replication_target) != 2:
49
+ raise ValueError("replication_target must be (constraint, ttl) tuple")
50
+ constraint, ttl = self.replication_target
51
+ # accept "≥3 geo-disjoint" / ">=3 geo-disjoint" / "k=3"
52
+ if not re.search(r"\d+", constraint):
53
+ raise ValueError(f"replication_target constraint must contain a count: {constraint!r}")
54
+ if not re.fullmatch(r"TTL=\d+d", ttl):
55
+ raise ValueError(f"replication_target TTL must look like 'TTL=180d': {ttl!r}")
56
+
57
+ if self.erasure_coding is not None:
58
+ k, n = self.erasure_coding
59
+ if not (re.match(r"k=\d+", k) and re.match(r"n=\d+", n)):
60
+ raise ValueError(f"erasure_coding must be ('k=N', 'n=M'): {self.erasure_coding!r}")
61
+ k_v = int(k.split("=")[1])
62
+ n_v = int(n.split("=")[1])
63
+ if k_v >= n_v or k_v <= 0:
64
+ raise ValueError(f"erasure_coding requires 0 < k < n; got k={k_v}, n={n_v}")
65
+
66
+ @property
67
+ def replication_min_count(self) -> int:
68
+ """Extract the minimum replica count from the constraint string."""
69
+ m = re.search(r"\d+", self.replication_target[0])
70
+ return int(m.group()) if m else 1
71
+
72
+ @property
73
+ def replication_ttl_days(self) -> int:
74
+ m = re.match(r"TTL=(\d+)d", self.replication_target[1])
75
+ return int(m.group(1)) if m else 0
76
+
77
+ def matches(self, *, topic: str, claim_type: str, publisher_rep: float) -> bool:
78
+ """Should this pinner replicate the claim?"""
79
+ if publisher_rep < self.publishers_min_rep:
80
+ return False
81
+ if self.claim_types != "*" and claim_type not in self.claim_types:
82
+ return False
83
+ if not any(_topic_match(topic, t) for t in self.topics):
84
+ return False
85
+ return True
86
+
87
+ def compose(self, *others: PinPolicy) -> PinPolicy:
88
+ """Union semantics: a claim matches if ANY composed policy matches."""
89
+ topics = list(self.topics)
90
+ for o in others:
91
+ for t in o.topics:
92
+ if t not in topics:
93
+ topics.append(t)
94
+ # Take the loosest min-rep across the set.
95
+ min_rep = min(self.publishers_min_rep, *[o.publishers_min_rep for o in others])
96
+ return PinPolicy(
97
+ topics=topics,
98
+ publishers_min_rep=min_rep,
99
+ claim_types=self.claim_types,
100
+ replication_target=self.replication_target,
101
+ erasure_coding=self.erasure_coding,
102
+ max_disk_gb=self.max_disk_gb + sum(o.max_disk_gb for o in others),
103
+ eviction=self.eviction,
104
+ cite_depth=max(self.cite_depth, *[o.cite_depth for o in others]),
105
+ )
106
+
107
+
108
+ def _topic_match(topic: str, pattern: str) -> bool:
109
+ """Glob-style topic matcher: `ai-safety/*` matches `ai-safety/eval`."""
110
+ if pattern == topic:
111
+ return True
112
+ if pattern == "*":
113
+ return True
114
+ if pattern.endswith("/*"):
115
+ return topic.startswith(pattern[:-1])
116
+ return False
hypermind/py.typed ADDED
File without changes
@@ -0,0 +1,87 @@
1
+ """LLM responders for ``HyperMindAgent.consult(...)``.
2
+
3
+ A *responder* is a callable matching the consult signature::
4
+
5
+ Callable[[bytes, str], Awaitable[Uncertainty]]
6
+
7
+ Given an agent kid and a free-form query, it returns an
8
+ :class:`Uncertainty` summarising that agent's belief.
9
+
10
+ The SDK ships:
11
+
12
+ * :class:`StaticResponder` — return a fixed Uncertainty (handy for tests
13
+ and deterministic simulations).
14
+ * :class:`SyntheticResponder` — sample a Beta(α, β) profile per agent
15
+ so different "personalities" answer differently. Used by the
16
+ geopolitical crisis simulation.
17
+ * :class:`OpenAIResponder` — call OpenAI Chat Completions, parse a
18
+ structured ``{confidence, reasoning}`` JSON response into Uncertainty.
19
+ * :class:`AnthropicResponder` — call Anthropic Messages, same contract.
20
+
21
+ Both LLM responders use a strict JSON-mode prompt so the SDK never has
22
+ to scrape free-text. The system prompt enforces a ``confidence`` field
23
+ in [0, 1] which maps to a Beta posterior of effective sample size 10.
24
+
25
+ Both LLM responders soft-depend on their respective SDKs — install
26
+ them via ``pip install hypermind[openai]`` or ``pip install hypermind[anthropic]``.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from hypermind.responders.anthropic import AnthropicResponder
32
+ from hypermind.responders.base import (
33
+ LLMResponderError,
34
+ Responder,
35
+ ResponderResult,
36
+ StaticResponder,
37
+ SyntheticResponder,
38
+ confidence_to_uncertainty,
39
+ unpack_responder_result,
40
+ )
41
+ from hypermind.responders.openai import OpenAIResponder
42
+ from hypermind.responders.router import (
43
+ ArchetypeMappedRouter,
44
+ CostAwareRouter,
45
+ MultiModelPanelRouter,
46
+ RandomPoolRouter,
47
+ ResponderRouter,
48
+ )
49
+ from hypermind.responders.structured import (
50
+ STRUCTURED_PROMPT_SUFFIX,
51
+ StructuredResponse,
52
+ TokenUsage,
53
+ parse_structured_response,
54
+ )
55
+ from hypermind.responders.tools import (
56
+ Tool,
57
+ ToolCallingResponder,
58
+ query_knowledge_base,
59
+ retrieve_document,
60
+ web_search,
61
+ )
62
+
63
+ __all__ = [
64
+ "STRUCTURED_PROMPT_SUFFIX",
65
+ "AnthropicResponder",
66
+ "ArchetypeMappedRouter",
67
+ "CostAwareRouter",
68
+ "LLMResponderError",
69
+ "MultiModelPanelRouter",
70
+ "OpenAIResponder",
71
+ "RandomPoolRouter",
72
+ "Responder",
73
+ "ResponderResult",
74
+ "ResponderRouter",
75
+ "StaticResponder",
76
+ "StructuredResponse",
77
+ "SyntheticResponder",
78
+ "TokenUsage",
79
+ "Tool",
80
+ "ToolCallingResponder",
81
+ "confidence_to_uncertainty",
82
+ "parse_structured_response",
83
+ "query_knowledge_base",
84
+ "retrieve_document",
85
+ "unpack_responder_result",
86
+ "web_search",
87
+ ]