hypermind 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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
+ )
@@ -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
+ }
@@ -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)