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,144 @@
|
|
|
1
|
+
"""DiversityGuard — block correlated synthesis.
|
|
2
|
+
|
|
3
|
+
Failure mode (design/failure-modes.md:13): if all panel members cite
|
|
4
|
+
the same upstream sources, their posteriors are not independent and
|
|
5
|
+
the synthesis aggregates the *same* signal multiple times — Condorcet
|
|
6
|
+
breakdown. Reputation does nothing about this.
|
|
7
|
+
|
|
8
|
+
The guard takes panel members' citation graphs (parent_kids per
|
|
9
|
+
member's most-recent claim on the topic) and computes pairwise
|
|
10
|
+
Jaccard overlap. If the mean overlap exceeds ``threshold``, the guard
|
|
11
|
+
flags ``epistemic_dependence`` in the synthesis payload — the caller
|
|
12
|
+
chooses whether to abort, downweight, or simply log.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class EpistemicDependence:
|
|
24
|
+
mean_jaccard: float
|
|
25
|
+
threshold: float
|
|
26
|
+
flagged: bool
|
|
27
|
+
pairs: int
|
|
28
|
+
|
|
29
|
+
def as_dict(self) -> dict[str, float | bool | int]:
|
|
30
|
+
return {
|
|
31
|
+
"mean_jaccard": self.mean_jaccard,
|
|
32
|
+
"threshold": self.threshold,
|
|
33
|
+
"flagged": self.flagged,
|
|
34
|
+
"pairs": self.pairs,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class QuorumResult:
|
|
40
|
+
"""T4-05 first-class quorum verdict."""
|
|
41
|
+
|
|
42
|
+
passed: bool
|
|
43
|
+
independent_count: int
|
|
44
|
+
required: int
|
|
45
|
+
mean_jaccard: float
|
|
46
|
+
|
|
47
|
+
def as_dict(self) -> dict[str, float | bool | int]:
|
|
48
|
+
return {
|
|
49
|
+
"passed": self.passed,
|
|
50
|
+
"independent_count": self.independent_count,
|
|
51
|
+
"required": self.required,
|
|
52
|
+
"mean_jaccard": self.mean_jaccard,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DiversityGuard:
|
|
58
|
+
threshold: float = 0.6
|
|
59
|
+
#: T4-05 — minimum number of pairwise-independent members required
|
|
60
|
+
#: for ``check_quorum`` to pass. Pairs with Jaccard ≥ ``threshold``
|
|
61
|
+
#: are treated as dependent.
|
|
62
|
+
quorum_k: int = 2
|
|
63
|
+
|
|
64
|
+
def check_quorum(
|
|
65
|
+
self,
|
|
66
|
+
panel: Sequence[Any],
|
|
67
|
+
) -> QuorumResult:
|
|
68
|
+
"""Return True iff ≥ ``quorum_k`` panel members are sufficiently
|
|
69
|
+
independent on cited evidence.
|
|
70
|
+
|
|
71
|
+
Accepts panel members that expose either a ``citations`` /
|
|
72
|
+
``parent_kids`` attribute or are themselves a sequence of bytes.
|
|
73
|
+
"""
|
|
74
|
+
citations: list[list[bytes]] = []
|
|
75
|
+
for m in panel:
|
|
76
|
+
cs = (
|
|
77
|
+
getattr(m, "citations", None)
|
|
78
|
+
or getattr(m, "parent_kids", None)
|
|
79
|
+
or (m if isinstance(m, (list, tuple, set)) else None)
|
|
80
|
+
)
|
|
81
|
+
citations.append([bytes(c) for c in (cs or [])])
|
|
82
|
+
# Build an independence graph: members are independent if their
|
|
83
|
+
# pairwise Jaccard < threshold.
|
|
84
|
+
indep_set: list[int] = []
|
|
85
|
+
sets = [set(cs) for cs in citations]
|
|
86
|
+
for i, s_i in enumerate(sets):
|
|
87
|
+
ok = True
|
|
88
|
+
for j in indep_set:
|
|
89
|
+
s_j = sets[j]
|
|
90
|
+
union = s_i | s_j
|
|
91
|
+
jacc = len(s_i & s_j) / len(union) if union else 0.0
|
|
92
|
+
if jacc >= self.threshold:
|
|
93
|
+
ok = False
|
|
94
|
+
break
|
|
95
|
+
if ok:
|
|
96
|
+
indep_set.append(i)
|
|
97
|
+
# Compute mean overlap for diagnostics.
|
|
98
|
+
total = 0.0
|
|
99
|
+
n = 0
|
|
100
|
+
non_empty = [s for s in sets if s]
|
|
101
|
+
for i in range(len(non_empty)):
|
|
102
|
+
for j in range(i + 1, len(non_empty)):
|
|
103
|
+
a, b = non_empty[i], non_empty[j]
|
|
104
|
+
union = a | b
|
|
105
|
+
if not union:
|
|
106
|
+
continue
|
|
107
|
+
total += len(a & b) / len(union)
|
|
108
|
+
n += 1
|
|
109
|
+
mean = total / n if n else 0.0
|
|
110
|
+
return QuorumResult(
|
|
111
|
+
passed=len(indep_set) >= self.quorum_k,
|
|
112
|
+
independent_count=len(indep_set),
|
|
113
|
+
required=self.quorum_k,
|
|
114
|
+
mean_jaccard=mean,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def assess(
|
|
118
|
+
self,
|
|
119
|
+
member_citations: Sequence[Sequence[bytes]],
|
|
120
|
+
) -> EpistemicDependence:
|
|
121
|
+
sets = [set(bytes(p) for p in cs) for cs in member_citations]
|
|
122
|
+
# Drop members with no citations — they contribute no overlap signal.
|
|
123
|
+
sets = [s for s in sets if s]
|
|
124
|
+
if len(sets) < 2:
|
|
125
|
+
return EpistemicDependence(
|
|
126
|
+
mean_jaccard=0.0, threshold=self.threshold, flagged=False, pairs=0
|
|
127
|
+
)
|
|
128
|
+
total = 0.0
|
|
129
|
+
n = 0
|
|
130
|
+
for i in range(len(sets)):
|
|
131
|
+
for j in range(i + 1, len(sets)):
|
|
132
|
+
a, b = sets[i], sets[j]
|
|
133
|
+
union = a | b
|
|
134
|
+
if not union:
|
|
135
|
+
continue
|
|
136
|
+
total += len(a & b) / len(union)
|
|
137
|
+
n += 1
|
|
138
|
+
mean = total / n if n else 0.0
|
|
139
|
+
return EpistemicDependence(
|
|
140
|
+
mean_jaccard=mean,
|
|
141
|
+
threshold=self.threshold,
|
|
142
|
+
flagged=mean >= self.threshold,
|
|
143
|
+
pairs=n,
|
|
144
|
+
)
|
hypermind/mind/evolve.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""SelfReflector — evolve from your own track record.
|
|
2
|
+
|
|
3
|
+
Reads the agent's own per-topic Beta posterior trajectory and adjusts
|
|
4
|
+
bound policy thresholds. Closes the loop the spec already named
|
|
5
|
+
"evolve" but never wired: outcomes → policy updates → better future
|
|
6
|
+
actions.
|
|
7
|
+
|
|
8
|
+
Three components:
|
|
9
|
+
SelfReflector — periodic introspection that ratchets policy thresholds
|
|
10
|
+
BrierFeedback — pulls v0.6 Brier shrinkage into working_memory posteriors
|
|
11
|
+
MentorFollowing — weights consult results from better-calibrated peers higher
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from hypermind.knowledge.reputation import ReputationKernel
|
|
21
|
+
from hypermind.mind.policy import (
|
|
22
|
+
ActionPolicy,
|
|
23
|
+
CheckpointOnSignalGain,
|
|
24
|
+
ConsultOnDisagreement,
|
|
25
|
+
DisputeOnContradiction,
|
|
26
|
+
LeverageCap,
|
|
27
|
+
PublishWhenConfident,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SelfReflector:
|
|
33
|
+
"""Adjust policy thresholds based on the agent's own outcome history.
|
|
34
|
+
|
|
35
|
+
Heuristic:
|
|
36
|
+
* Falling momentum_z (z < -0.5) AND recent publishes → tighten
|
|
37
|
+
PublishWhenConfident (raise threshold by +0.05 up to 0.97).
|
|
38
|
+
* Stable, high momentum (z > +0.5) → relax
|
|
39
|
+
PublishWhenConfident (lower threshold by -0.02 down to 0.6).
|
|
40
|
+
* Brier > 0.25 → tighten
|
|
41
|
+
DisputeOnContradiction (raise min_rep_delta by +0.05).
|
|
42
|
+
|
|
43
|
+
Idempotent — calling reflect() twice in a row without new outcomes
|
|
44
|
+
is a no-op. Operates in-place on the policy objects.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
agent_kid: bytes
|
|
48
|
+
topic: str
|
|
49
|
+
reputation: ReputationKernel
|
|
50
|
+
publish_floor: float = 0.6
|
|
51
|
+
publish_ceiling: float = 0.97
|
|
52
|
+
publish_step: float = 0.05
|
|
53
|
+
dispute_step: float = 0.05
|
|
54
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
def reflect(self, policies: Iterable[ActionPolicy]) -> dict[str, Any]:
|
|
57
|
+
snap = self.reputation.snapshot(self.agent_kid, self.topic)
|
|
58
|
+
# Brier may not be wired in this kernel until first oracle resolves.
|
|
59
|
+
brier_adj = self.reputation.calibration_adjustment(self.agent_kid, self.topic)
|
|
60
|
+
# adjustment ∈ [0.5, 2.0]; values < 1 mean "this agent is overconfident".
|
|
61
|
+
is_overconfident = brier_adj < 1.0
|
|
62
|
+
is_underconfident = brier_adj > 1.0
|
|
63
|
+
|
|
64
|
+
action_log: dict[str, Any] = {
|
|
65
|
+
"score": snap.score,
|
|
66
|
+
"z": snap.momentum_z,
|
|
67
|
+
"n": snap.n_observations,
|
|
68
|
+
"brier_adj": brier_adj,
|
|
69
|
+
"changes": [],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for pol in policies:
|
|
73
|
+
inner = pol.inner if isinstance(pol, LeverageCap) else pol
|
|
74
|
+
if isinstance(inner, PublishWhenConfident):
|
|
75
|
+
old = inner.threshold
|
|
76
|
+
if is_overconfident or snap.momentum_z < -0.5:
|
|
77
|
+
new = min(self.publish_ceiling, old + self.publish_step)
|
|
78
|
+
elif is_underconfident and snap.momentum_z > 0.5:
|
|
79
|
+
new = max(self.publish_floor, old - (self.publish_step / 2.0))
|
|
80
|
+
else:
|
|
81
|
+
new = old
|
|
82
|
+
if new != old:
|
|
83
|
+
inner.threshold = new
|
|
84
|
+
action_log["changes"].append(
|
|
85
|
+
{"policy": "PublishWhenConfident", "threshold": [old, new]}
|
|
86
|
+
)
|
|
87
|
+
elif isinstance(inner, DisputeOnContradiction):
|
|
88
|
+
old = inner.min_rep_delta
|
|
89
|
+
if is_overconfident:
|
|
90
|
+
new = min(0.5, old + self.dispute_step)
|
|
91
|
+
else:
|
|
92
|
+
new = old
|
|
93
|
+
if new != old:
|
|
94
|
+
inner.min_rep_delta = new
|
|
95
|
+
action_log["changes"].append(
|
|
96
|
+
{"policy": "DisputeOnContradiction", "min_rep_delta": [old, new]}
|
|
97
|
+
)
|
|
98
|
+
elif isinstance(inner, ConsultOnDisagreement):
|
|
99
|
+
# If we're often wrong, consult sooner (lower divergence threshold).
|
|
100
|
+
old = inner.divergence
|
|
101
|
+
if is_overconfident:
|
|
102
|
+
new = max(0.1, old - 0.05)
|
|
103
|
+
else:
|
|
104
|
+
new = old
|
|
105
|
+
if new != old:
|
|
106
|
+
inner.divergence = new
|
|
107
|
+
action_log["changes"].append(
|
|
108
|
+
{"policy": "ConsultOnDisagreement", "divergence": [old, new]}
|
|
109
|
+
)
|
|
110
|
+
elif isinstance(inner, CheckpointOnSignalGain):
|
|
111
|
+
# No change here — KL threshold isn't outcome-driven.
|
|
112
|
+
pass
|
|
113
|
+
self.history.append(action_log)
|
|
114
|
+
return action_log
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class BrierFeedback:
|
|
119
|
+
"""Apply Brier shrinkage directly to working_memory posteriors.
|
|
120
|
+
|
|
121
|
+
An overconfident agent's working_memory[topic] posterior is pulled
|
|
122
|
+
toward 0.5 by the calibration_adjustment factor — so the *next*
|
|
123
|
+
publish is automatically more humble.
|
|
124
|
+
|
|
125
|
+
S4 — When ``persona`` is set, the post-shrinkage value is also
|
|
126
|
+
written through to the KernelStore on disk so the next sim that
|
|
127
|
+
instantiates this persona starts with a calibrated prior. Optional;
|
|
128
|
+
persona="" keeps the legacy in-process-only behaviour.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
agent_kid: bytes
|
|
132
|
+
reputation: ReputationKernel
|
|
133
|
+
persona: str = ""
|
|
134
|
+
|
|
135
|
+
def apply(self, working_memory: dict[str, Any], topic: str) -> float | None:
|
|
136
|
+
posteriors = working_memory.setdefault("posteriors", {})
|
|
137
|
+
score = posteriors.get(topic)
|
|
138
|
+
if score is None:
|
|
139
|
+
return None
|
|
140
|
+
adj = self.reputation.calibration_adjustment(self.agent_kid, topic)
|
|
141
|
+
if adj >= 1.0:
|
|
142
|
+
return score # well-calibrated or no signal yet → leave alone
|
|
143
|
+
# Pull toward 0.5 by (1 - adj) — adj=0.5 ⇒ shrink halfway.
|
|
144
|
+
shrink = 1.0 - adj
|
|
145
|
+
new = score + (0.5 - score) * shrink
|
|
146
|
+
posteriors[topic] = new
|
|
147
|
+
# S4 — best-effort persistence to KernelStore. Never raise back
|
|
148
|
+
# to the agent; calibration improvement is operational, not load-
|
|
149
|
+
# bearing for any spec invariant.
|
|
150
|
+
if self.persona:
|
|
151
|
+
try:
|
|
152
|
+
from hypermind.knowledge.kernel_store import KernelStore
|
|
153
|
+
|
|
154
|
+
KernelStore.update(self.persona, topic, posterior=new)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
return new
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class MentorFollowing:
|
|
162
|
+
"""Weight consult results by mentor calibration quality.
|
|
163
|
+
|
|
164
|
+
Given (peer_kid → posterior) returned from a panel, returns a
|
|
165
|
+
re-weighted aggregate where each peer's vote is multiplied by their
|
|
166
|
+
Brier-derived calibration adjustment (≥1 = better than us, <1 =
|
|
167
|
+
worse). Normalises so weights sum to 1.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
reputation: ReputationKernel
|
|
171
|
+
|
|
172
|
+
def reweight(
|
|
173
|
+
self,
|
|
174
|
+
topic: str,
|
|
175
|
+
votes: dict[bytes, float],
|
|
176
|
+
) -> tuple[dict[bytes, float], float]:
|
|
177
|
+
weights: dict[bytes, float] = {}
|
|
178
|
+
for kid in votes:
|
|
179
|
+
adj = self.reputation.calibration_adjustment(bytes(kid), topic)
|
|
180
|
+
weights[bytes(kid)] = max(0.05, adj)
|
|
181
|
+
total = sum(weights.values()) or 1.0
|
|
182
|
+
norm = {k: w / total for k, w in weights.items()}
|
|
183
|
+
weighted = sum(votes[k] * norm[bytes(k)] for k in votes)
|
|
184
|
+
return norm, weighted
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Autonomous federation — bridges that decide when to relay.
|
|
2
|
+
|
|
3
|
+
Wraps the existing cross-namespace consult pattern (see
|
|
4
|
+
examples/scenario_cross_namespace_federation.py) with a deliberation
|
|
5
|
+
trigger: the bridge agent's deliberator initiates a foreign-namespace
|
|
6
|
+
consult ONLY when the home namespace is uncertain AND the foreign
|
|
7
|
+
namespace has enough qualified agents.
|
|
8
|
+
|
|
9
|
+
Foreign reputation is never merged into the home kernel — it is
|
|
10
|
+
displayed in the synthesis payload, mirroring the existing v0.4
|
|
11
|
+
invariant. We add a strict ``never_merge`` assertion as a guard rail.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from hypermind.agent import HyperMindAgent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FederationPolicy:
|
|
26
|
+
"""Decision rule for when to relay a query across namespaces."""
|
|
27
|
+
|
|
28
|
+
home_uncertainty_threshold: float = 0.3
|
|
29
|
+
"""Trigger when the home consult has stdev >= threshold."""
|
|
30
|
+
|
|
31
|
+
min_foreign_qualified: int = 2
|
|
32
|
+
"""Need at least this many foreign agents above the rep floor."""
|
|
33
|
+
|
|
34
|
+
foreign_rep_floor: float = 0.55
|
|
35
|
+
"""Foreign agents must be at least this reputable on the topic."""
|
|
36
|
+
|
|
37
|
+
def should_relay(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
home_disagreement: float,
|
|
41
|
+
foreign_qualified: int,
|
|
42
|
+
) -> bool:
|
|
43
|
+
return (
|
|
44
|
+
home_disagreement >= self.home_uncertainty_threshold
|
|
45
|
+
and foreign_qualified >= self.min_foreign_qualified
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def assert_never_merge(home_state: dict[str, Any], foreign_payload: dict[str, Any]) -> None:
|
|
50
|
+
"""Guard: foreign reputation must NEVER appear inside home kernel keys.
|
|
51
|
+
|
|
52
|
+
Raises ValueError if a foreign reputation map has bled into the
|
|
53
|
+
home agent's local kernel snapshot. Used by federation tests.
|
|
54
|
+
"""
|
|
55
|
+
home_kids = set(home_state.get("home_reputation", {}).keys())
|
|
56
|
+
foreign_kids = set(foreign_payload.get("foreign_reputation", {}).keys())
|
|
57
|
+
leaked = home_kids & foreign_kids
|
|
58
|
+
if leaked:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"FEDERATION-NEVER-MERGE violation: foreign kids {sorted(leaked)} "
|
|
61
|
+
f"appear in home reputation map. Foreign reputation must remain "
|
|
62
|
+
f"displayed-only (per scenario_cross_namespace_federation invariant)."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def auto_relay(
|
|
67
|
+
bridge: HyperMindAgent,
|
|
68
|
+
*,
|
|
69
|
+
topic: str,
|
|
70
|
+
foreign_namespace_id: bytes,
|
|
71
|
+
foreign_candidate_kids: Iterable[bytes],
|
|
72
|
+
policy: FederationPolicy | None = None,
|
|
73
|
+
caller_disagreement_hint: float | None = None,
|
|
74
|
+
) -> dict[str, Any] | None:
|
|
75
|
+
"""Run the deliberator-style federation check; relay if the policy fires.
|
|
76
|
+
|
|
77
|
+
The caller may pass ``caller_disagreement_hint`` to suggest an
|
|
78
|
+
expected home-namespace disagreement value, but it is treated as a
|
|
79
|
+
HINT only — never as authoritative input. The bridge always
|
|
80
|
+
measures the actual disagreement from its own home-namespace state
|
|
81
|
+
(reputation-snapshot stdev across home agents on the topic) and
|
|
82
|
+
clamps the caller's hint so it can only LOWER the value, never
|
|
83
|
+
inflate it. This closes a vulnerability where a caller could pass
|
|
84
|
+
``home_disagreement=999`` to bypass the policy gate.
|
|
85
|
+
|
|
86
|
+
Returns the synthesis payload (foreign rep displayed-only) or None
|
|
87
|
+
if the policy declined to relay. The bridge's *own* consult and
|
|
88
|
+
synthesize verbs are used — no bypass of the agent API.
|
|
89
|
+
"""
|
|
90
|
+
policy = policy or FederationPolicy()
|
|
91
|
+
world = getattr(bridge, "_world", None)
|
|
92
|
+
if world is None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Measure home-namespace disagreement from the bridge's own state:
|
|
96
|
+
# stdev of reputation snapshots across home agents on the topic.
|
|
97
|
+
home_world = world
|
|
98
|
+
measured_disagreement = 0.0
|
|
99
|
+
scores = [
|
|
100
|
+
home_world.reputation.snapshot(kid, topic).score
|
|
101
|
+
for kid in home_world.agents.keys()
|
|
102
|
+
if kid != bridge.keypair.kid and topic in home_world.agents[kid].rooms
|
|
103
|
+
]
|
|
104
|
+
if len(scores) >= 2:
|
|
105
|
+
mean = sum(scores) / len(scores)
|
|
106
|
+
var = sum((s - mean) ** 2 for s in scores) / len(scores)
|
|
107
|
+
import math
|
|
108
|
+
|
|
109
|
+
measured_disagreement = math.sqrt(var)
|
|
110
|
+
|
|
111
|
+
# Caller's hint can only LOWER the measured value, never inflate it.
|
|
112
|
+
if caller_disagreement_hint is not None:
|
|
113
|
+
home_disagreement = min(caller_disagreement_hint, measured_disagreement)
|
|
114
|
+
else:
|
|
115
|
+
home_disagreement = measured_disagreement
|
|
116
|
+
|
|
117
|
+
qualified = 0
|
|
118
|
+
foreign_rep: dict[bytes, float] = {}
|
|
119
|
+
# Look up the foreign namespace's reputation kernel via the registry.
|
|
120
|
+
from hypermind.agent import _lookup_namespace # local import to avoid cycle
|
|
121
|
+
|
|
122
|
+
foreign_world = _lookup_namespace(bytes(foreign_namespace_id))
|
|
123
|
+
if foreign_world is None:
|
|
124
|
+
return None
|
|
125
|
+
for kid in foreign_candidate_kids:
|
|
126
|
+
snap = foreign_world.reputation.snapshot(bytes(kid), topic)
|
|
127
|
+
foreign_rep[bytes(kid)] = snap.score
|
|
128
|
+
if snap.score >= policy.foreign_rep_floor:
|
|
129
|
+
qualified += 1
|
|
130
|
+
|
|
131
|
+
if not policy.should_relay(
|
|
132
|
+
home_disagreement=home_disagreement,
|
|
133
|
+
foreign_qualified=qualified,
|
|
134
|
+
):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Best-effort cross-namespace consult.
|
|
138
|
+
try:
|
|
139
|
+
result = await bridge.consult(
|
|
140
|
+
query=f"federation:{topic}",
|
|
141
|
+
topic=topic,
|
|
142
|
+
panel_size=min(3, qualified),
|
|
143
|
+
cross_namespace=[bytes(foreign_namespace_id)],
|
|
144
|
+
)
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
return {
|
|
148
|
+
"topic": topic,
|
|
149
|
+
"foreign_namespace_id": bytes(foreign_namespace_id).hex(),
|
|
150
|
+
"foreign_reputation": {k.hex(): v for k, v in foreign_rep.items()},
|
|
151
|
+
"aggregated_posterior": float(result.posterior.mean()),
|
|
152
|
+
"panel_size": len(result.panel),
|
|
153
|
+
"rule": "FEDERATION-AUTO-RELAY",
|
|
154
|
+
}
|
hypermind/mind/goals.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Goals — what an autonomous agent is trying to achieve.
|
|
2
|
+
|
|
3
|
+
A Goal is a declarative target: a topic, a confidence threshold, an
|
|
4
|
+
optional deadline, and an optional success predicate. The Deliberator
|
|
5
|
+
walks active goals each tick and picks an action per the bound policies.
|
|
6
|
+
|
|
7
|
+
Goals are immutable. State (ACTIVE / SATISFIED / EXPIRED / ABANDONED)
|
|
8
|
+
lives on a paired GoalStatus row in the queue.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import StrEnum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GoalOutcome(StrEnum):
|
|
22
|
+
ACTIVE = "active"
|
|
23
|
+
SATISFIED = "satisfied"
|
|
24
|
+
EXPIRED = "expired"
|
|
25
|
+
ABANDONED = "abandoned"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Goal:
|
|
30
|
+
"""A declarative target an agent will autonomously pursue."""
|
|
31
|
+
|
|
32
|
+
topic: str
|
|
33
|
+
target_confidence: float = 0.8
|
|
34
|
+
deadline_ms: int | None = None # wall-clock ms; None = no deadline
|
|
35
|
+
success_predicate: Callable[[dict[str, Any]], bool] | None = None
|
|
36
|
+
description: str = ""
|
|
37
|
+
goal_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
38
|
+
created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if not 0.0 < self.target_confidence <= 1.0:
|
|
42
|
+
raise ValueError(f"target_confidence must be in (0, 1]; got {self.target_confidence}")
|
|
43
|
+
if not self.topic:
|
|
44
|
+
raise ValueError("topic must be a non-empty string")
|
|
45
|
+
|
|
46
|
+
def is_expired(self, *, now_ms: int | None = None) -> bool:
|
|
47
|
+
if self.deadline_ms is None:
|
|
48
|
+
return False
|
|
49
|
+
return (now_ms if now_ms is not None else int(time.time() * 1000)) >= self.deadline_ms
|
|
50
|
+
|
|
51
|
+
def is_satisfied(self, working_memory: dict[str, Any]) -> bool:
|
|
52
|
+
"""Default satisfaction: posterior_for(topic) >= target_confidence.
|
|
53
|
+
|
|
54
|
+
A custom success_predicate overrides this entirely.
|
|
55
|
+
"""
|
|
56
|
+
if self.success_predicate is not None:
|
|
57
|
+
return bool(self.success_predicate(working_memory))
|
|
58
|
+
posteriors = working_memory.get("posteriors", {})
|
|
59
|
+
score = posteriors.get(self.topic)
|
|
60
|
+
return score is not None and score >= self.target_confidence
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class GoalStatus:
|
|
65
|
+
goal: Goal
|
|
66
|
+
outcome: GoalOutcome = GoalOutcome.ACTIVE
|
|
67
|
+
last_action: str | None = None
|
|
68
|
+
last_action_ms: int | None = None
|
|
69
|
+
actions_taken: int = 0
|
|
70
|
+
notes: list[str] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GoalQueue:
|
|
74
|
+
"""In-memory queue of goals an agent is pursuing.
|
|
75
|
+
|
|
76
|
+
Not thread-safe; the Deliberator owns it within a single asyncio task.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self._statuses: dict[str, GoalStatus] = {}
|
|
81
|
+
|
|
82
|
+
def add(self, goal: Goal) -> GoalStatus:
|
|
83
|
+
if goal.goal_id in self._statuses:
|
|
84
|
+
return self._statuses[goal.goal_id]
|
|
85
|
+
status = GoalStatus(goal=goal)
|
|
86
|
+
self._statuses[goal.goal_id] = status
|
|
87
|
+
return status
|
|
88
|
+
|
|
89
|
+
def remove(self, goal_id: str) -> None:
|
|
90
|
+
self._statuses.pop(goal_id, None)
|
|
91
|
+
|
|
92
|
+
def active(self) -> list[GoalStatus]:
|
|
93
|
+
return [s for s in self._statuses.values() if s.outcome is GoalOutcome.ACTIVE]
|
|
94
|
+
|
|
95
|
+
def all(self) -> list[GoalStatus]:
|
|
96
|
+
return list(self._statuses.values())
|
|
97
|
+
|
|
98
|
+
def get(self, goal_id: str) -> GoalStatus | None:
|
|
99
|
+
return self._statuses.get(goal_id)
|
|
100
|
+
|
|
101
|
+
def reap(self, working_memory: dict[str, Any], *, now_ms: int | None = None) -> int:
|
|
102
|
+
"""Mark satisfied/expired goals; return the count reaped."""
|
|
103
|
+
reaped = 0
|
|
104
|
+
for status in self._statuses.values():
|
|
105
|
+
if status.outcome is not GoalOutcome.ACTIVE:
|
|
106
|
+
continue
|
|
107
|
+
if status.goal.is_satisfied(working_memory):
|
|
108
|
+
status.outcome = GoalOutcome.SATISFIED
|
|
109
|
+
reaped += 1
|
|
110
|
+
continue
|
|
111
|
+
if status.goal.is_expired(now_ms=now_ms):
|
|
112
|
+
status.outcome = GoalOutcome.EXPIRED
|
|
113
|
+
reaped += 1
|
|
114
|
+
return reaped
|
|
115
|
+
|
|
116
|
+
def __len__(self) -> int:
|
|
117
|
+
return len(self._statuses)
|