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,190 @@
1
+ """Bind a Deliberator to HyperMindAgent without editing agent.py.
2
+
3
+ We attach methods at import-time:
4
+ agent.bind_deliberator(deliberator) → store reference
5
+ agent.pursue(goal) → adds goal to bound deliberator
6
+ agent.tick() → advance the deliberator one step
7
+ agent.deliberator → get the bound deliberator (or None)
8
+ agent.plan_and_act(goal) → T4-03 plan-and-act primitive
9
+
10
+ This module is imported by ``hypermind.__init__`` so the API is
11
+ available the moment a user does ``import hypermind``. Agents
12
+ constructed without ``bind_deliberator`` behave exactly as before
13
+ (zero behavioural change to existing tests).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from hypermind.mind.deliberator import Deliberator, DeliberatorTick
21
+ from hypermind.mind.goals import Goal, GoalStatus
22
+
23
+ if TYPE_CHECKING:
24
+ from hypermind.agent import HyperMindAgent
25
+ from hypermind.workflow.engine import WorkflowState
26
+
27
+ _ATTR = "_mind_deliberator"
28
+
29
+
30
+ def bind_deliberator(self: HyperMindAgent, deliberator: Deliberator) -> Deliberator:
31
+ setattr(self, _ATTR, deliberator)
32
+ return deliberator
33
+
34
+
35
+ def get_deliberator(self: HyperMindAgent) -> Deliberator | None:
36
+ return getattr(self, _ATTR, None)
37
+
38
+
39
+ def pursue(self: HyperMindAgent, goal: Goal) -> GoalStatus:
40
+ delib = get_deliberator(self)
41
+ if delib is None:
42
+ # Auto-bind a default deliberator the first time pursue() is called.
43
+ delib = Deliberator(self)
44
+ bind_deliberator(self, delib)
45
+ return delib.pursue(goal)
46
+
47
+
48
+ async def tick(self: HyperMindAgent) -> DeliberatorTick:
49
+ delib = get_deliberator(self)
50
+ if delib is None:
51
+ raise RuntimeError(
52
+ "tick() requires a bound Deliberator.\n"
53
+ " Hint: agent.bind_deliberator(Deliberator(agent, policies=[...])) first, "
54
+ "or call agent.pursue(goal) to auto-bind a default deliberator."
55
+ )
56
+ return await delib.tick()
57
+
58
+
59
+ async def plan_and_act(
60
+ self: HyperMindAgent,
61
+ goal: Goal,
62
+ *,
63
+ executors: dict[str, Any] | None = None,
64
+ max_replans: int = 1,
65
+ ) -> WorkflowState:
66
+ """T4-03 plan-and-act primitive — decompose goal → Plan → Workflow → run.
67
+
68
+ 1. Use :class:`InformationGainPlanner` to decompose ``goal`` into a
69
+ :class:`Plan` (a :class:`PlanDAG` with one node per step).
70
+ 2. Compile :meth:`Plan.to_workflow` and register the resulting body
71
+ as a one-shot ``@workflow`` so :class:`Workflow.run` can drive it.
72
+ 3. Run via ``await Workflow.run``. If a :class:`RepairOnDispute`
73
+ observation arrives mid-run via the bound deliberator, the
74
+ :class:`DefaultRepairPolicy` produces a replan; this method
75
+ catches the replan and re-runs (up to ``max_replans`` times).
76
+
77
+ ``executors`` lets callers inject per-node coroutines; structural
78
+ nodes without an executor become no-op steps that record their
79
+ payload (so a bare goal still produces a runnable workflow).
80
+ """
81
+ from hypermind.mind.active import CandidateQuestion, InformationGainPlanner
82
+ from hypermind.workflow.engine import Workflow
83
+ from hypermind.workflow.engine import workflow as workflow_decorator
84
+ from hypermind.workflow.plan import Plan, PlanDAG, PlanNode
85
+ from hypermind.workflow.repair import DefaultRepairPolicy, WorkflowDisputeContext
86
+
87
+ planner = InformationGainPlanner()
88
+ # Decompose goal into a small candidate question set; one node per
89
+ # candidate plus a final synthesis node. The scoring is informational
90
+ # only — the planner has already shaped the DAG topology.
91
+ candidates = [
92
+ CandidateQuestion(
93
+ qid=f"q-{i}",
94
+ topic=goal.topic,
95
+ expected_posterior=0.5,
96
+ )
97
+ for i in range(2)
98
+ ]
99
+ ranked = planner.rank(candidates)
100
+
101
+ dag = PlanDAG()
102
+ dag.add_node(
103
+ PlanNode(
104
+ node_id="goal_root",
105
+ kind="goal",
106
+ payload={
107
+ "topic": goal.topic,
108
+ "target_confidence": goal.target_confidence,
109
+ "rule_id": "MIND-PLAN-AND-ACT",
110
+ },
111
+ )
112
+ )
113
+ prev = "goal_root"
114
+ for q in ranked:
115
+ node_id = f"act-{q.qid}"
116
+ dag.add_node(
117
+ PlanNode(
118
+ node_id=node_id,
119
+ kind="action",
120
+ payload={"qid": q.qid, "topic": q.topic},
121
+ )
122
+ )
123
+ dag.add_edge(prev, node_id, "depends_on")
124
+ prev = node_id
125
+ dag.add_node(
126
+ PlanNode(
127
+ node_id="synthesise",
128
+ kind="fanin",
129
+ payload={"rule_id": "MIND-PLAN-SYNTH"},
130
+ )
131
+ )
132
+ dag.add_edge(prev, "synthesise", "depends_on")
133
+
134
+ plan = Plan(
135
+ dag=dag,
136
+ executors=dict(executors or {}),
137
+ name=f"plan-and-act-{goal.goal_id}",
138
+ )
139
+
140
+ body = plan.to_workflow()
141
+ # Register under a unique name so multiple pursue() calls don't clash.
142
+ workflow_decorator(name=plan.name)(body)
143
+
144
+ # Run; on dispute observation in the deliberator, replan once.
145
+ state: WorkflowState = await Workflow.run(body)
146
+
147
+ delib = get_deliberator(self)
148
+ replans = 0
149
+ while delib is not None and replans < max_replans:
150
+ repair_obs = next(
151
+ (
152
+ o
153
+ for o in delib._observations # type: ignore[attr-defined]
154
+ if o.get("kind") == "dispute_close" and o.get("workflow_id") == state.workflow_id
155
+ ),
156
+ None,
157
+ )
158
+ if repair_obs is None:
159
+ break
160
+ # A dispute close arrived for this workflow — emit a fresh plan.
161
+ repair = DefaultRepairPolicy()
162
+ outcome = type(
163
+ "_O",
164
+ (),
165
+ {
166
+ "final_state": repair_obs.get("final_state", "UNRESOLVED_PARTITION"),
167
+ "target_kid": repair_obs.get("target_kid", b""),
168
+ },
169
+ )()
170
+ repair_plan = repair.repair(WorkflowDisputeContext(workflow_state=state, outcome=outcome))
171
+ repair_body = repair_plan.to_workflow()
172
+ workflow_decorator(name=repair_plan.name)(repair_body)
173
+ state = await Workflow.run(repair_body)
174
+ replans += 1
175
+
176
+ return state
177
+
178
+
179
+ def install() -> None:
180
+ """Attach the mind methods onto HyperMindAgent. Idempotent."""
181
+ from hypermind.agent import HyperMindAgent
182
+
183
+ if getattr(HyperMindAgent, "_mind_installed", False):
184
+ return
185
+ HyperMindAgent.bind_deliberator = bind_deliberator # type: ignore[attr-defined]
186
+ HyperMindAgent.pursue = pursue # type: ignore[attr-defined]
187
+ HyperMindAgent.tick = tick # type: ignore[attr-defined]
188
+ HyperMindAgent.plan_and_act = plan_and_act # type: ignore[attr-defined]
189
+ HyperMindAgent.deliberator = property(get_deliberator) # type: ignore[attr-defined]
190
+ HyperMindAgent._mind_installed = True # type: ignore[attr-defined]
@@ -0,0 +1,74 @@
1
+ """Deterministic mediator selection (closes ROADMAP gap 6).
2
+
3
+ When a dispute reaches COUNTER state without resolution, a mediator is
4
+ selected from a verifiable random pool. The selection is a verifiable
5
+ random function over (dispute_kid, sorted(eligible_pool)): every
6
+ participant computes the same selection, so deadlock is impossible
7
+ without collusion exceeding the eligible pool's honest majority.
8
+
9
+ VRF here is HMAC-SHA256 (deterministic, publicly verifiable given
10
+ inputs). Future versions may upgrade to Ed25519-VRF once a vetted
11
+ Python implementation lands; the API does not change.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import hmac
18
+ from collections.abc import Iterable, Sequence
19
+ from dataclasses import dataclass
20
+
21
+ from hypermind.knowledge.reputation import ReputationKernel
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class MediatorSelector:
26
+ topic: str
27
+ min_reputation: float = 0.6
28
+ min_observations: int = 30
29
+
30
+ def eligible(
31
+ self,
32
+ candidates: Iterable[bytes],
33
+ *,
34
+ reputation: ReputationKernel,
35
+ exclude: Iterable[bytes] = (),
36
+ ) -> list[bytes]:
37
+ excluded = {bytes(k) for k in exclude}
38
+ out: list[bytes] = []
39
+ for kid in candidates:
40
+ kid_b = bytes(kid)
41
+ if kid_b in excluded:
42
+ continue
43
+ snap = reputation.snapshot(kid_b, self.topic)
44
+ if snap.score < self.min_reputation:
45
+ continue
46
+ if snap.n_observations < self.min_observations:
47
+ continue
48
+ out.append(kid_b)
49
+ # Stable order — required for determinism.
50
+ out.sort()
51
+ return out
52
+
53
+
54
+ def select_mediator(
55
+ *,
56
+ dispute_kid: bytes,
57
+ eligible_pool: Sequence[bytes],
58
+ ) -> bytes:
59
+ """VRF-style deterministic pick from a sorted eligible pool.
60
+
61
+ ``dispute_kid`` MUST be 32 bytes (the SHA-256 kid of the dispute
62
+ record); the eligible pool MUST be the canonical sorted output of
63
+ ``MediatorSelector.eligible``. Identical inputs across nodes ⇒
64
+ identical mediator.
65
+ """
66
+ if len(dispute_kid) != 32:
67
+ raise ValueError("dispute_kid must be 32 bytes")
68
+ if not eligible_pool:
69
+ raise ValueError("eligible_pool must be non-empty")
70
+ pool = sorted(set(bytes(k) for k in eligible_pool))
71
+ # HMAC the concatenated pool with the dispute_kid as the key.
72
+ mac = hmac.new(dispute_kid, b"".join(pool), hashlib.sha256).digest()
73
+ idx = int.from_bytes(mac, "big") % len(pool)
74
+ return pool[idx]
@@ -0,0 +1,129 @@
1
+ """Per-agent private memory over the RelataDB Memory API (integration P1).
2
+
3
+ This is the *unsigned* half of the storage story (path b). Where
4
+ :class:`~hypermind.storage.relata.RelataBackend` persists the verifiable ledger
5
+ (signed claims, disputes, reputation, Merkle log), a ``RelataMemoryStore`` is an
6
+ agent's private scratchpad of learnings and experiences — bi-temporal,
7
+ provenance-stamped, and recallable, but **not** part of the federation ledger.
8
+
9
+ Scoping mirrors the ledger backend:
10
+
11
+ * namespace → tenant (``X-Organization-Id`` via ``extra_headers``)
12
+ * agent kid → session (``session_id`` — per-agent memory partition)
13
+
14
+ The ``relata`` SDK is imported lazily inside :meth:`open`, so this module imports
15
+ without the optional ``hypermind[relata]`` extra. Tests inject ``memory_factory``.
16
+
17
+ Citation bridge: :func:`sources_from_memory` turns memory ids into PROV-O
18
+ provenance descriptors suitable to embed in a signed claim payload under a
19
+ ``"sources"`` key. Note this is deliberately *separate* from a claim's ``cites``
20
+ field — ``cites`` is the intra-federation claim-kid DAG (walked by
21
+ ``CitationGraph`` with a cycle check); ``sources`` are external evidence
22
+ references (Relata memory ids + provenance), which are not claim kids.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from collections.abc import Callable
28
+ from typing import TYPE_CHECKING, Any
29
+
30
+ if TYPE_CHECKING:
31
+ from hypermind.wire import Kid
32
+
33
+ _PURPOSE = "agent_notes"
34
+
35
+ # Returns an object exposing add()/search()/justify()/close() — i.e. relata.Memory.
36
+ MemoryFactory = Callable[[], Any]
37
+
38
+
39
+ class RelataMemoryStore:
40
+ """One agent's private memory. Bound to (server, namespace, agent kid)."""
41
+
42
+ def __init__(
43
+ self,
44
+ base_url: str,
45
+ *,
46
+ namespace_id: str,
47
+ agent_kid: Kid | bytes,
48
+ purpose: str = _PURPOSE,
49
+ bearer_token: str | None = None,
50
+ memory_factory: MemoryFactory | None = None,
51
+ ) -> None:
52
+ self._url = base_url
53
+ self._ns = namespace_id
54
+ self._session = bytes(agent_kid).hex()
55
+ self._purpose = purpose
56
+ self._tok = bearer_token
57
+ self._factory = memory_factory
58
+ self._m: Any = None
59
+
60
+ def open(self) -> None:
61
+ if self._m is not None:
62
+ return
63
+ if self._factory is not None:
64
+ self._m = self._factory()
65
+ else: # pragma: no cover - requires the optional `relata` SDK + live server
66
+ from relata import Memory
67
+
68
+ self._m = Memory(
69
+ self._url,
70
+ purpose=self._purpose,
71
+ bearer_token=self._tok,
72
+ session_id=self._session, # per-agent partition
73
+ extra_headers={"X-Organization-Id": self._ns}, # tenant isolation
74
+ )
75
+
76
+ def close(self) -> None:
77
+ if self._m is not None:
78
+ self._m.close()
79
+ self._m = None
80
+
81
+ def _require(self) -> Any:
82
+ if self._m is None:
83
+ raise RuntimeError("RelataMemoryStore used before open()")
84
+ return self._m
85
+
86
+ # ---- verbs ----------------------------------------------------------
87
+
88
+ def remember(
89
+ self, content: str, *, confidence: float = 1.0, memory_class: str = "semantic"
90
+ ) -> str:
91
+ """Store a learning/experience; return its memory id."""
92
+ return str(self._require().add(content, confidence=confidence, memory_class=memory_class))
93
+
94
+ def remember_batch(
95
+ self,
96
+ items: "list[str | dict]",
97
+ *,
98
+ session_id: str | None = None,
99
+ ) -> list[str]:
100
+ """Store many learnings in one request; return their ids in order.
101
+
102
+ Each item is a ``str`` (content only) or a ``dict`` with ``content``
103
+ and optional ``confidence`` / ``memory_class`` / ``purpose``.
104
+ Delegates to ``POST /memory/remember/batch`` — amortises per-request
105
+ overhead and the index flush across the whole batch.
106
+ """
107
+ return list(self._require().add_batch(items, session_id=session_id))
108
+
109
+ def recall(self, query: str, *, top_k: int = 5, as_of: str | None = None) -> list[dict]:
110
+ """Ranked recall (confidence × recency × relevance).
111
+
112
+ Pass ``as_of`` (a timestamp) for bi-temporal recall — "what did this
113
+ agent know at T?".
114
+ """
115
+ return list(self._require().search(query, top_k=top_k, as_of=as_of))
116
+
117
+ def justify(self, memory_id: str) -> dict:
118
+ """Return the PROV-O provenance chain for ``memory_id``."""
119
+ return dict(self._require().justify(memory_id))
120
+
121
+
122
+ def sources_from_memory(store: RelataMemoryStore, memory_ids: list[str]) -> list[dict]:
123
+ """Resolve memory ids into provenance descriptors for a claim payload.
124
+
125
+ Embed the result under a ``"sources"`` key in a ``publish_claim`` payload to
126
+ give a signed research output verifiable citations back to the evidence the
127
+ agent used — "no source = not in the output".
128
+ """
129
+ return [{"memory_id": mid, "provenance": store.justify(mid)} for mid in memory_ids]
@@ -0,0 +1,307 @@
1
+ """ActionPolicy — pure decisions about what an agent should do next.
2
+
3
+ Policies are stateless functions over a PolicyContext. They emit an
4
+ ActionDecision (or None for "no action this tick"). The Deliberator
5
+ collects decisions from all bound policies and picks the highest-priority
6
+ one for dispatch.
7
+
8
+ Built-ins:
9
+ PublishWhenConfident — auto-publish when posterior crosses threshold
10
+ ConsultOnDisagreement — auto-consult when subscribed claims diverge
11
+ DisputeOnContradiction — auto-dispute when peer contradicts and rep gap > δ
12
+ CheckpointOnSignalGain — auto-checkpoint when working_memory KL > threshold
13
+ LeverageCap — guard wrapping any policy: bound action impact
14
+ by historical Brier (anti calibration-laundering)
15
+
16
+ Every Decision carries a ``rule_id`` matching the structured-refusal
17
+ contract in src/hypermind/rule_ids.py — observability + governance live
18
+ on this id.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+ from collections.abc import Sequence
25
+ from dataclasses import dataclass, field
26
+ from enum import StrEnum
27
+ from typing import Any
28
+
29
+
30
+ class ActionKind(StrEnum):
31
+ PUBLISH = "publish"
32
+ CONSULT = "consult"
33
+ DISPUTE = "dispute"
34
+ CHECKPOINT = "checkpoint"
35
+ INTENT = "intent" # broadcast a mind/intent record
36
+ DISSENT = "dissent" # broadcast a mind/dissent record
37
+ NOOP = "noop"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class PolicyContext:
42
+ """Snapshot of relevant agent state at a single deliberation tick.
43
+
44
+ Frozen — policies must not mutate. The Deliberator builds one fresh
45
+ each tick from the live agent.
46
+ """
47
+
48
+ topic: str
49
+ target_confidence: float
50
+ working_memory: dict[str, Any]
51
+ own_brier: float | None = None
52
+ own_reputation: float | None = None
53
+ recent_observations: tuple[dict[str, Any], ...] = () # claims seen on the topic
54
+ last_action: str | None = None
55
+ last_checkpoint_posterior: float | None = None
56
+ consult_in_flight: bool = False
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ActionDecision:
61
+ kind: ActionKind
62
+ rule_id: str
63
+ priority: int # higher wins when multiple policies fire
64
+ payload: dict[str, Any] = field(default_factory=dict)
65
+ reason: str = ""
66
+
67
+
68
+ class ActionPolicy:
69
+ """Base class — subclass and implement ``decide``.
70
+
71
+ Subclasses MUST be pure: same context → same decision.
72
+ """
73
+
74
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None: # pragma: no cover - abstract
75
+ raise NotImplementedError
76
+
77
+
78
+ class NoOpPolicy(ActionPolicy):
79
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
80
+ return None
81
+
82
+
83
+ # -----------------------------------------------------------------------
84
+ # Built-in policies
85
+ # -----------------------------------------------------------------------
86
+
87
+
88
+ @dataclass
89
+ class PublishWhenConfident(ActionPolicy):
90
+ """Publish a claim once the posterior on the goal topic crosses the threshold.
91
+
92
+ Only fires when (a) we have a posterior for the goal topic AND (b)
93
+ we have not just published in the last tick (avoids storms). The
94
+ SelfReflector may ratchet ``threshold`` up if past publishes get
95
+ disputed too often.
96
+ """
97
+
98
+ threshold: float = 0.8
99
+ priority: int = 50
100
+
101
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
102
+ posteriors = ctx.working_memory.get("posteriors", {})
103
+ score = posteriors.get(ctx.topic)
104
+ if score is None:
105
+ return None
106
+ effective = max(self.threshold, ctx.target_confidence)
107
+ if score < effective:
108
+ return None
109
+ if ctx.last_action == "publish":
110
+ return None
111
+ return ActionDecision(
112
+ kind=ActionKind.PUBLISH,
113
+ rule_id="MIND-PUBLISH-CONFIDENT",
114
+ priority=self.priority,
115
+ payload={"posterior": score, "threshold": effective},
116
+ reason=f"posterior {score:.3f} ≥ threshold {effective:.3f}",
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class ConsultOnDisagreement(ActionPolicy):
122
+ """Trigger a consult when subscribed peers disagree on the topic.
123
+
124
+ Disagreement = stdev of recent peer posteriors > divergence.
125
+ """
126
+
127
+ divergence: float = 0.3
128
+ min_observations: int = 2
129
+ priority: int = 40
130
+
131
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
132
+ if ctx.consult_in_flight:
133
+ return None
134
+ scores = [
135
+ float(o["posterior"])
136
+ for o in ctx.recent_observations
137
+ if o.get("topic") == ctx.topic and "posterior" in o
138
+ ]
139
+ if len(scores) < self.min_observations:
140
+ return None
141
+ mean = sum(scores) / len(scores)
142
+ var = sum((s - mean) ** 2 for s in scores) / len(scores)
143
+ std = math.sqrt(var)
144
+ if std < self.divergence:
145
+ return None
146
+ return ActionDecision(
147
+ kind=ActionKind.CONSULT,
148
+ rule_id="MIND-CONSULT-DISAGREEMENT",
149
+ priority=self.priority,
150
+ payload={"observed_stdev": std, "divergence_threshold": self.divergence},
151
+ reason=f"peer posterior stdev {std:.3f} ≥ {self.divergence:.3f}",
152
+ )
153
+
154
+
155
+ @dataclass
156
+ class DisputeOnContradiction(ActionPolicy):
157
+ """Auto-dispute a peer claim when it contradicts ours by a wide margin.
158
+
159
+ Fires when we have an own posterior on the topic and a recent peer
160
+ claim has the opposite verdict (one above 0.5, the other below) by
161
+ at least ``min_gap`` AND our reputation gap (own − peer) ≥
162
+ ``min_rep_delta``. Avoids low-rep agents picking fights with
163
+ high-rep agents.
164
+ """
165
+
166
+ min_gap: float = 0.4
167
+ min_rep_delta: float = 0.15
168
+ priority: int = 60
169
+
170
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
171
+ own_score = (ctx.working_memory.get("posteriors") or {}).get(ctx.topic)
172
+ if own_score is None or ctx.own_reputation is None:
173
+ return None
174
+ for obs in ctx.recent_observations:
175
+ if obs.get("topic") != ctx.topic:
176
+ continue
177
+ peer_score = obs.get("posterior")
178
+ peer_rep = obs.get("reputation")
179
+ target_kid = obs.get("kid")
180
+ if peer_score is None or peer_rep is None or target_kid is None:
181
+ continue
182
+ # Opposite verdicts only.
183
+ if (own_score - 0.5) * (peer_score - 0.5) >= 0:
184
+ continue
185
+ if abs(own_score - peer_score) < self.min_gap:
186
+ continue
187
+ if (ctx.own_reputation - peer_rep) < self.min_rep_delta:
188
+ continue
189
+ return ActionDecision(
190
+ kind=ActionKind.DISPUTE,
191
+ rule_id="MIND-DISPUTE-CONTRADICTION",
192
+ priority=self.priority,
193
+ payload={
194
+ "target_kid": target_kid,
195
+ "own_score": own_score,
196
+ "peer_score": peer_score,
197
+ "rep_delta": ctx.own_reputation - peer_rep,
198
+ },
199
+ reason=(
200
+ f"contradiction gap {abs(own_score - peer_score):.2f}, "
201
+ f"rep advantage {(ctx.own_reputation - peer_rep):.2f}"
202
+ ),
203
+ )
204
+ return None
205
+
206
+
207
+ @dataclass
208
+ class CheckpointOnSignalGain(ActionPolicy):
209
+ """Auto-checkpoint when working_memory has shifted enough since last seal.
210
+
211
+ Uses Bernoulli KL divergence on the topic's posterior between the
212
+ last checkpoint and now. Cheap, scale-invariant, and intuitive
213
+ ("posterior moved noticeably" → seal a snapshot).
214
+ """
215
+
216
+ kl_threshold: float = 0.1
217
+ priority: int = 30
218
+
219
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
220
+ score = (ctx.working_memory.get("posteriors") or {}).get(ctx.topic)
221
+ if score is None:
222
+ return None
223
+ if ctx.last_checkpoint_posterior is None:
224
+ return ActionDecision(
225
+ kind=ActionKind.CHECKPOINT,
226
+ rule_id="MIND-CHECKPOINT-FIRST",
227
+ priority=self.priority,
228
+ payload={"posterior": score},
229
+ reason="initial checkpoint",
230
+ )
231
+ kl = _bernoulli_kl(score, ctx.last_checkpoint_posterior)
232
+ if kl < self.kl_threshold:
233
+ return None
234
+ return ActionDecision(
235
+ kind=ActionKind.CHECKPOINT,
236
+ rule_id="MIND-CHECKPOINT-SIGNAL-GAIN",
237
+ priority=self.priority,
238
+ payload={"posterior": score, "kl": kl},
239
+ reason=f"KL {kl:.3f} ≥ threshold {self.kl_threshold:.3f}",
240
+ )
241
+
242
+
243
+ def _bernoulli_kl(p: float, q: float) -> float:
244
+ """KL(Bern(p) || Bern(q)). Clamps to a tiny eps to avoid log(0)."""
245
+ eps = 1e-9
246
+ p = min(max(p, eps), 1.0 - eps)
247
+ q = min(max(q, eps), 1.0 - eps)
248
+ return p * math.log(p / q) + (1 - p) * math.log((1 - p) / (1 - q))
249
+
250
+
251
+ # -----------------------------------------------------------------------
252
+ # Adversarial guard: LeverageCap
253
+ # -----------------------------------------------------------------------
254
+
255
+
256
+ @dataclass
257
+ class LeverageCap(ActionPolicy):
258
+ """Wrap an inner policy and veto/attenuate its publish actions.
259
+
260
+ Protects against calibration-laundering (failure-modes.md:15): an
261
+ agent with great historical reputation but a known bad calibration
262
+ on this topic should not be able to broadcast a high-leverage claim.
263
+
264
+ The cap is a multiplicative factor applied to *claim weight* (passed
265
+ into the publish payload as ``leverage_factor``). Downstream consult
266
+ aggregation may use this when computing influence. ``own_brier``
267
+ above ``brier_ceiling`` triggers the cap.
268
+ """
269
+
270
+ inner: ActionPolicy
271
+ brier_ceiling: float = 0.25
272
+ min_leverage: float = 0.2
273
+
274
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
275
+ decision = self.inner.decide(ctx)
276
+ if decision is None:
277
+ return None
278
+ if decision.kind is not ActionKind.PUBLISH:
279
+ return decision
280
+ brier = ctx.own_brier
281
+ if brier is None or brier <= self.brier_ceiling:
282
+ return decision
283
+ # Linear interpolation: brier=ceiling → 1.0; brier=2*ceiling → min_leverage.
284
+ ratio = (brier - self.brier_ceiling) / max(self.brier_ceiling, 1e-9)
285
+ leverage = max(self.min_leverage, 1.0 - ratio)
286
+ new_payload = dict(decision.payload)
287
+ new_payload["leverage_factor"] = leverage
288
+ new_payload["leverage_reason"] = "MIND-LEVERAGE-CAP"
289
+ return ActionDecision(
290
+ kind=decision.kind,
291
+ rule_id="MIND-LEVERAGE-CAP",
292
+ priority=decision.priority,
293
+ payload=new_payload,
294
+ reason=(
295
+ f"{decision.reason}; leverage capped to {leverage:.2f} "
296
+ f"(brier {brier:.3f} > {self.brier_ceiling:.3f})"
297
+ ),
298
+ )
299
+
300
+
301
+ def select_top_decision(decisions: Sequence[ActionDecision | None]) -> ActionDecision | None:
302
+ """Pick the highest-priority decision; ties broken by rule_id sort."""
303
+ live = [d for d in decisions if d is not None]
304
+ if not live:
305
+ return None
306
+ live.sort(key=lambda d: (-d.priority, d.rule_id))
307
+ return live[0]