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,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]
|
hypermind/mind/policy.py
ADDED
|
@@ -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]
|