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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
hypermind/pin_policy.py
ADDED
|
@@ -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
|
+
]
|