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