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
hypermind/sync.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Synchronous wrapper around :class:`hypermind.HyperMindAgent`.
|
|
2
|
+
|
|
3
|
+
Some integrators (notebooks, CLIs, simple scripts) cannot or do not
|
|
4
|
+
want to use ``async``/``await``. ``SyncAgent`` is a thin wrapper that
|
|
5
|
+
exposes every public async verb of ``HyperMindAgent`` as a sync method.
|
|
6
|
+
|
|
7
|
+
Implementation strategy (Jupyter-safe):
|
|
8
|
+
|
|
9
|
+
* If the calling thread already has a running event loop (e.g. inside
|
|
10
|
+
a Jupyter cell), ``SyncAgent`` lazily spawns a dedicated worker
|
|
11
|
+
thread that owns its own ``asyncio.new_event_loop()`` and uses
|
|
12
|
+
``asyncio.run_coroutine_threadsafe`` to dispatch coroutines to it.
|
|
13
|
+
* Otherwise, ``asyncio.run`` is used per call.
|
|
14
|
+
|
|
15
|
+
The wrapper is generated by **introspection** — every ``async def``
|
|
16
|
+
public method on :class:`HyperMindAgent` is auto-wrapped, so there is
|
|
17
|
+
nothing to keep in sync by hand. Sync property/passthrough access for
|
|
18
|
+
non-async helpers (``working_memory``, ``advance_time``, etc.) is also
|
|
19
|
+
forwarded.
|
|
20
|
+
|
|
21
|
+
ROADMAP §0.5.7. Stable from v0.5; signatures track ``HyperMindAgent``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import inspect
|
|
28
|
+
import threading
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
from hypermind.agent import HyperMindAgent
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
34
|
+
from collections.abc import Coroutine
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------
|
|
38
|
+
# Module-level worker loop (Jupyter-safe path).
|
|
39
|
+
# ---------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
_worker_lock = threading.Lock()
|
|
42
|
+
_worker_loop: asyncio.AbstractEventLoop | None = None
|
|
43
|
+
_worker_thread: threading.Thread | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ensure_worker_loop() -> asyncio.AbstractEventLoop:
|
|
47
|
+
"""Lazily start the dedicated worker thread + loop.
|
|
48
|
+
|
|
49
|
+
The worker is daemonised so it does not block interpreter shutdown.
|
|
50
|
+
"""
|
|
51
|
+
global _worker_loop, _worker_thread
|
|
52
|
+
with _worker_lock:
|
|
53
|
+
if _worker_loop is not None and _worker_thread is not None and _worker_thread.is_alive():
|
|
54
|
+
return _worker_loop
|
|
55
|
+
loop = asyncio.new_event_loop()
|
|
56
|
+
ready = threading.Event()
|
|
57
|
+
|
|
58
|
+
def _run() -> None:
|
|
59
|
+
asyncio.set_event_loop(loop)
|
|
60
|
+
ready.set()
|
|
61
|
+
loop.run_forever()
|
|
62
|
+
|
|
63
|
+
thread = threading.Thread(target=_run, name="hypermind-sync-worker", daemon=True)
|
|
64
|
+
thread.start()
|
|
65
|
+
ready.wait()
|
|
66
|
+
_worker_loop = loop
|
|
67
|
+
_worker_thread = thread
|
|
68
|
+
return loop
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _run_coro(coro: Coroutine[Any, Any, Any]) -> Any:
|
|
72
|
+
"""Run a coroutine to completion from a synchronous context.
|
|
73
|
+
|
|
74
|
+
If the current thread already has a running loop (Jupyter), schedule
|
|
75
|
+
on the worker loop via ``run_coroutine_threadsafe``; otherwise use
|
|
76
|
+
``asyncio.run``.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
asyncio.get_running_loop()
|
|
80
|
+
running_loop = True
|
|
81
|
+
except RuntimeError:
|
|
82
|
+
running_loop = False
|
|
83
|
+
if running_loop:
|
|
84
|
+
loop = _ensure_worker_loop()
|
|
85
|
+
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
86
|
+
return fut.result()
|
|
87
|
+
return asyncio.run(coro)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------
|
|
91
|
+
# SyncAgent
|
|
92
|
+
# ---------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SyncAgent:
|
|
96
|
+
"""Synchronous wrapper around a single :class:`HyperMindAgent`.
|
|
97
|
+
|
|
98
|
+
Every public ``async def`` method of ``HyperMindAgent`` is exposed
|
|
99
|
+
as a sync method here with the same signature minus the ``async``.
|
|
100
|
+
Non-async public attributes (e.g. ``working_memory``, ``namespace``,
|
|
101
|
+
``rooms``) are forwarded through ``__getattr__``.
|
|
102
|
+
|
|
103
|
+
Use :meth:`SyncAgent.sandbox` or :meth:`SyncAgent.testbed_cohort` to
|
|
104
|
+
construct.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
__slots__ = ("_agent",)
|
|
108
|
+
|
|
109
|
+
def __init__(self, agent: HyperMindAgent) -> None:
|
|
110
|
+
object.__setattr__(self, "_agent", agent)
|
|
111
|
+
|
|
112
|
+
# ---- factories -----------------------------------------------------
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def sandbox(cls, **kwargs: Any) -> SyncAgent:
|
|
116
|
+
"""Sync analogue of :meth:`HyperMindAgent.sandbox`."""
|
|
117
|
+
return cls(HyperMindAgent.sandbox(**kwargs))
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def testbed_cohort(cls, n: int = 2, **kwargs: Any) -> list[SyncAgent]:
|
|
121
|
+
"""Sync analogue of :func:`hypermind.testing.testbed_cohort`.
|
|
122
|
+
|
|
123
|
+
Returns a plain list of wrapped agents. Caller is responsible for
|
|
124
|
+
calling :meth:`close` (or using :class:`contextlib.ExitStack`) —
|
|
125
|
+
there is no async-with cleanup in the sync surface.
|
|
126
|
+
"""
|
|
127
|
+
agents = HyperMindAgent.testbed(agents=n, **kwargs)
|
|
128
|
+
return [cls(a) for a in agents]
|
|
129
|
+
|
|
130
|
+
# ---- context-manager passthrough ----------------------------------
|
|
131
|
+
|
|
132
|
+
def __enter__(self) -> SyncAgent:
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def __exit__(self, *_exc: Any) -> None:
|
|
136
|
+
self.close()
|
|
137
|
+
|
|
138
|
+
# ---- forwarded attribute access -----------------------------------
|
|
139
|
+
|
|
140
|
+
def __getattr__(self, name: str) -> Any:
|
|
141
|
+
# Avoid recursion on lookup of private slot.
|
|
142
|
+
if name == "_agent":
|
|
143
|
+
raise AttributeError(name)
|
|
144
|
+
agent = object.__getattribute__(self, "_agent")
|
|
145
|
+
attr = getattr(agent, name)
|
|
146
|
+
if inspect.iscoroutinefunction(attr):
|
|
147
|
+
|
|
148
|
+
def _sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
149
|
+
return _run_coro(attr(*args, **kwargs))
|
|
150
|
+
|
|
151
|
+
_sync_wrapper.__name__ = name
|
|
152
|
+
_sync_wrapper.__doc__ = attr.__doc__
|
|
153
|
+
return _sync_wrapper
|
|
154
|
+
if inspect.isasyncgenfunction(attr):
|
|
155
|
+
# Drain an async generator into a synchronous list. For
|
|
156
|
+
# truly streaming outputs, callers SHOULD stay on
|
|
157
|
+
# ``HyperMindAgent`` and the async path; the sync surface
|
|
158
|
+
# materialises the full sequence in memory.
|
|
159
|
+
def _sync_drain(*args: Any, **kwargs: Any) -> list[Any]:
|
|
160
|
+
async def _collect() -> list[Any]:
|
|
161
|
+
out: list[Any] = []
|
|
162
|
+
async for item in attr(*args, **kwargs):
|
|
163
|
+
out.append(item)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
return _run_coro(_collect())
|
|
167
|
+
|
|
168
|
+
_sync_drain.__name__ = name
|
|
169
|
+
_sync_drain.__doc__ = attr.__doc__
|
|
170
|
+
return _sync_drain
|
|
171
|
+
return attr
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------
|
|
175
|
+
# Verb-parity assertion (introspection-driven test surface).
|
|
176
|
+
# ---------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _public_async_verbs(cls: type) -> list[str]:
|
|
180
|
+
"""Return sorted names of public ``async def`` (or async-generator)
|
|
181
|
+
methods of ``cls``."""
|
|
182
|
+
out: list[str] = []
|
|
183
|
+
for name, member in inspect.getmembers(cls):
|
|
184
|
+
if name.startswith("_"):
|
|
185
|
+
continue
|
|
186
|
+
if inspect.iscoroutinefunction(member) or inspect.isasyncgenfunction(member):
|
|
187
|
+
out.append(name)
|
|
188
|
+
return sorted(out)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["SyncAgent", "_public_async_verbs"]
|
hypermind/tal.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Trust & Access Layer (TAL) — reputation-gated Rooms with vouch decay.
|
|
2
|
+
|
|
3
|
+
Implements the application-layer membership control described in
|
|
4
|
+
``docs/spec/07-trust-access-layer.md``: Rooms with a per-Room minimum
|
|
5
|
+
reputation, vouches with HLC timestamps, and exponential decay applied
|
|
6
|
+
to old vouch contributions.
|
|
7
|
+
|
|
8
|
+
This module is the v0.1 surface — a minimal subset of §7 sufficient to
|
|
9
|
+
gate ``discover_mentors`` / ``request_vouch`` and unblock the federated
|
|
10
|
+
learning loop. The full §7 wire messages (ROOM_DEFINE, ROOM_JOIN,
|
|
11
|
+
VOUCH, VOUCH_REVOKE) land in v0.2.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
from hypermind.knowledge.reputation import ReputationKernel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TALError(Exception):
|
|
22
|
+
"""Raised when a TAL operation violates a Room policy."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Room:
|
|
27
|
+
room_id: str
|
|
28
|
+
min_reputation: float # 0.0 = open
|
|
29
|
+
members: set[bytes] = field(default_factory=set)
|
|
30
|
+
creator_kid: bytes = b""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TrustStatus:
|
|
35
|
+
in_room: bool
|
|
36
|
+
reputation_snapshot: float
|
|
37
|
+
vouch_count: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class _VouchRecord:
|
|
42
|
+
voucher_kid: bytes
|
|
43
|
+
vouchee_kid: bytes
|
|
44
|
+
room_id: str
|
|
45
|
+
topic: str
|
|
46
|
+
issued_hlc_ms: int
|
|
47
|
+
initial_alpha_contribution: float
|
|
48
|
+
remaining_factor: float = 1.0 # 1.0 = no decay yet
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Decay rate per day (matches reputation kernel's smooth-slash λ).
|
|
52
|
+
_DECAY_LAMBDA_PER_DAY = 0.02
|
|
53
|
+
_MS_PER_DAY = 1000 * 60 * 60 * 24
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TAL:
|
|
57
|
+
"""Rooms + vouch log with lazy decay, layered on a ReputationKernel."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, reputation_kernel: ReputationKernel) -> None:
|
|
60
|
+
self._rep = reputation_kernel
|
|
61
|
+
self._rooms: dict[str, Room] = {}
|
|
62
|
+
self._vouch_log: list[_VouchRecord] = []
|
|
63
|
+
|
|
64
|
+
# ---- Rooms ----------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def create_room(self, room_id: str, min_reputation: float, creator_kid: bytes) -> Room:
|
|
67
|
+
if room_id in self._rooms:
|
|
68
|
+
raise TALError(f"room {room_id!r} already exists")
|
|
69
|
+
room = Room(
|
|
70
|
+
room_id=room_id,
|
|
71
|
+
min_reputation=min_reputation,
|
|
72
|
+
members={bytes(creator_kid)},
|
|
73
|
+
creator_kid=bytes(creator_kid),
|
|
74
|
+
)
|
|
75
|
+
self._rooms[room_id] = room
|
|
76
|
+
return room
|
|
77
|
+
|
|
78
|
+
def get_room(self, room_id: str) -> Room | None:
|
|
79
|
+
return self._rooms.get(room_id)
|
|
80
|
+
|
|
81
|
+
def join_room(self, agent_kid: bytes, room_id: str) -> None:
|
|
82
|
+
room = self._rooms.get(room_id)
|
|
83
|
+
if room is None:
|
|
84
|
+
raise TALError(f"room {room_id!r} does not exist")
|
|
85
|
+
if room.min_reputation > 0.0:
|
|
86
|
+
best = self._max_reputation(agent_kid)
|
|
87
|
+
if best < room.min_reputation:
|
|
88
|
+
raise TALError("reputation below room minimum")
|
|
89
|
+
room.members.add(bytes(agent_kid))
|
|
90
|
+
|
|
91
|
+
def _max_reputation(self, agent_kid: bytes) -> float:
|
|
92
|
+
"""Best score across every (agent, topic) the kernel has seen."""
|
|
93
|
+
kid = bytes(agent_kid)
|
|
94
|
+
best = 0.0
|
|
95
|
+
for (a, topic), _p in self._rep._posteriors.items():
|
|
96
|
+
if a == kid:
|
|
97
|
+
score = self._rep.snapshot(kid, topic).score
|
|
98
|
+
if score > best:
|
|
99
|
+
best = score
|
|
100
|
+
return best
|
|
101
|
+
|
|
102
|
+
# ---- Vouches --------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def vouch(
|
|
105
|
+
self,
|
|
106
|
+
voucher_kid: bytes,
|
|
107
|
+
vouchee_kid: bytes,
|
|
108
|
+
room_id: str,
|
|
109
|
+
topic: str,
|
|
110
|
+
*,
|
|
111
|
+
now_hlc_ms: int = 0,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Record a vouch and delegate the α contribution to the kernel.
|
|
114
|
+
|
|
115
|
+
The reputation kernel applies the 5%-of-α-above-prior cap; the
|
|
116
|
+
vouch log here is the bookkeeping needed for ``decay_vouches``.
|
|
117
|
+
"""
|
|
118
|
+
if room_id not in self._rooms:
|
|
119
|
+
raise TALError(f"room {room_id!r} does not exist")
|
|
120
|
+
voucher_kid = bytes(voucher_kid)
|
|
121
|
+
vouchee_kid = bytes(vouchee_kid)
|
|
122
|
+
before = self._rep._get(vouchee_kid, topic).alpha
|
|
123
|
+
self._rep.vouch(voucher_kid, vouchee_kid, topic, strength=1.0)
|
|
124
|
+
after = self._rep._get(vouchee_kid, topic).alpha
|
|
125
|
+
contribution = max(0.0, after - before)
|
|
126
|
+
self._vouch_log.append(
|
|
127
|
+
_VouchRecord(
|
|
128
|
+
voucher_kid=voucher_kid,
|
|
129
|
+
vouchee_kid=vouchee_kid,
|
|
130
|
+
room_id=room_id,
|
|
131
|
+
topic=topic,
|
|
132
|
+
issued_hlc_ms=int(now_hlc_ms),
|
|
133
|
+
initial_alpha_contribution=contribution,
|
|
134
|
+
remaining_factor=1.0,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def decay_vouches(self, now_hlc_ms: int) -> None:
|
|
139
|
+
"""Exponentially decay vouches older than 24h.
|
|
140
|
+
|
|
141
|
+
For each vouch in the log: compute days_elapsed since issue,
|
|
142
|
+
target_factor = max(0, 1 - λ · days_elapsed). Reduce the
|
|
143
|
+
vouchee's α by the difference between the previously-applied
|
|
144
|
+
factor and the new one. Bounded below at 0 — never refunds.
|
|
145
|
+
"""
|
|
146
|
+
for v in self._vouch_log:
|
|
147
|
+
elapsed_ms = max(0, now_hlc_ms - v.issued_hlc_ms)
|
|
148
|
+
elapsed_days = elapsed_ms / _MS_PER_DAY
|
|
149
|
+
if elapsed_days < 1.0:
|
|
150
|
+
continue
|
|
151
|
+
new_factor = max(0.0, 1.0 - _DECAY_LAMBDA_PER_DAY * elapsed_days)
|
|
152
|
+
if new_factor >= v.remaining_factor:
|
|
153
|
+
continue # nothing to remove
|
|
154
|
+
delta = (v.remaining_factor - new_factor) * v.initial_alpha_contribution
|
|
155
|
+
if delta <= 0:
|
|
156
|
+
continue
|
|
157
|
+
p = self._rep._get(v.vouchee_kid, v.topic)
|
|
158
|
+
p.alpha = max(1.0, p.alpha - delta)
|
|
159
|
+
v.remaining_factor = new_factor
|
|
160
|
+
|
|
161
|
+
# ---- Queries --------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def trust_query(self, kid: bytes, room_id: str) -> TrustStatus:
|
|
164
|
+
kid = bytes(kid)
|
|
165
|
+
room = self._rooms.get(room_id)
|
|
166
|
+
in_room = bool(room and kid in room.members)
|
|
167
|
+
rep = self._max_reputation(kid)
|
|
168
|
+
vouch_count = sum(
|
|
169
|
+
1 for v in self._vouch_log if v.vouchee_kid == kid and v.room_id == room_id
|
|
170
|
+
)
|
|
171
|
+
return TrustStatus(
|
|
172
|
+
in_room=in_room,
|
|
173
|
+
reputation_snapshot=rep,
|
|
174
|
+
vouch_count=vouch_count,
|
|
175
|
+
)
|
hypermind/tasks.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Task delegation primitives — spec §4.4.
|
|
2
|
+
|
|
3
|
+
v0.10.1 (T1-04 / T1-05) adds a :class:`TaskGateway` Protocol so
|
|
4
|
+
:func:`submit_task` can dispatch in-process *or* over a network
|
|
5
|
+
transport (MCP / libp2p). The default :class:`InProcessGateway`
|
|
6
|
+
preserves the v0.x asyncio-Future semantics; :class:`MCPGateway`
|
|
7
|
+
dispatches over an :class:`MCPClient` and awaits a remote
|
|
8
|
+
``submit_task`` reply.
|
|
9
|
+
|
|
10
|
+
Cancellation, timeout, and idempotent retry are gateway-level
|
|
11
|
+
concerns:
|
|
12
|
+
|
|
13
|
+
- :meth:`TaskHandle.cancel` flips the handle's status to ``cancelled``
|
|
14
|
+
and asks its gateway (if any) to propagate a TASK_CANCEL signal.
|
|
15
|
+
- :meth:`TaskHandle.result` uses :func:`asyncio.wait_for` for
|
|
16
|
+
timeouts; on timeout the handle is implicitly cancelled.
|
|
17
|
+
- :class:`MCPGateway` keys an in-memory cache by ``request_kid`` so a
|
|
18
|
+
duplicate :func:`submit_task` (same descriptor) returns the cached
|
|
19
|
+
:class:`TaskHandle` rather than dispatching twice.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import hashlib
|
|
26
|
+
import secrets
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
29
|
+
|
|
30
|
+
import cbor2
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
33
|
+
from hypermind.crypto.signing import Keypair
|
|
34
|
+
from hypermind.mcp.client import MCPClient
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TaskDescriptor:
|
|
39
|
+
"""The signed payload describing a unit of remote work.
|
|
40
|
+
|
|
41
|
+
Used by :class:`TaskGateway` implementations to dispatch a task
|
|
42
|
+
over a transport. Independent of any specific wire format —
|
|
43
|
+
gateways serialise the descriptor as they see fit.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
task_id: str
|
|
47
|
+
submitter_kid: bytes
|
|
48
|
+
assignee_kid: bytes
|
|
49
|
+
topic: str
|
|
50
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
timeout_s: float = 30.0
|
|
52
|
+
|
|
53
|
+
def to_canonical_cbor(self) -> bytes:
|
|
54
|
+
"""RFC 8949 §4.2 deterministic CBOR for hashing / idempotency."""
|
|
55
|
+
return cbor2.dumps(
|
|
56
|
+
{
|
|
57
|
+
"task_id": self.task_id,
|
|
58
|
+
"submitter_kid": self.submitter_kid,
|
|
59
|
+
"assignee_kid": self.assignee_kid,
|
|
60
|
+
"topic": self.topic,
|
|
61
|
+
"arguments": self.arguments,
|
|
62
|
+
"timeout_s": self.timeout_s,
|
|
63
|
+
},
|
|
64
|
+
canonical=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def request_kid(self) -> bytes:
|
|
69
|
+
"""SHA-256 of the canonical encoding — the idempotency key."""
|
|
70
|
+
return hashlib.sha256(self.to_canonical_cbor()).digest()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class TaskHandle:
|
|
75
|
+
"""Awaitable handle for a submitted task (spec §4.4).
|
|
76
|
+
|
|
77
|
+
Returned by :func:`submit_task`; callers ``await handle.result()`` to
|
|
78
|
+
block until the assignee completes the work or the timeout fires.
|
|
79
|
+
Cancellation propagates to the gateway so remote tasks receive a
|
|
80
|
+
TASK_CANCEL signal when possible.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
>>> handle = await submit_task(
|
|
84
|
+
... submitter_kid=alice.keypair.kid,
|
|
85
|
+
... assignee_kid=bob.keypair.kid,
|
|
86
|
+
... topic="ai-safety/eval",
|
|
87
|
+
... arguments={"claim_id": kid.hex()},
|
|
88
|
+
... timeout_s=30.0,
|
|
89
|
+
... )
|
|
90
|
+
>>> result = await handle.result(timeout=30.0)
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
task_id: str # hex uuid
|
|
94
|
+
submitter_kid: bytes
|
|
95
|
+
assignee_kid: bytes
|
|
96
|
+
topic: str
|
|
97
|
+
_result_future: asyncio.Future = field(
|
|
98
|
+
default_factory=lambda: asyncio.get_event_loop().create_future(),
|
|
99
|
+
repr=False,
|
|
100
|
+
)
|
|
101
|
+
_status: Literal["pending", "running", "complete", "failed"] = "pending"
|
|
102
|
+
_gateway: TaskGateway | None = field(default=None, repr=False)
|
|
103
|
+
_request_kid: bytes = field(default=b"", repr=False)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def status(self) -> str:
|
|
107
|
+
return self._status
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def request_kid(self) -> bytes:
|
|
111
|
+
return self._request_kid
|
|
112
|
+
|
|
113
|
+
async def result(self, timeout: float = 30.0) -> Any:
|
|
114
|
+
"""Wait for task completion.
|
|
115
|
+
|
|
116
|
+
Returns the result payload or raises TaskTimeoutError/TaskFailedError.
|
|
117
|
+
On timeout the handle transitions to ``cancelled`` and the
|
|
118
|
+
gateway is asked to propagate a TASK_CANCEL signal (T1-05).
|
|
119
|
+
"""
|
|
120
|
+
if self._status == "failed":
|
|
121
|
+
raise TaskFailedError(f"Task {self.task_id} failed or was cancelled")
|
|
122
|
+
try:
|
|
123
|
+
return await asyncio.wait_for(asyncio.shield(self._result_future), timeout=timeout)
|
|
124
|
+
except TimeoutError as e:
|
|
125
|
+
await self.cancel()
|
|
126
|
+
raise TaskTimeoutError(f"Task {self.task_id} timed out after {timeout}s") from e
|
|
127
|
+
except asyncio.CancelledError as e:
|
|
128
|
+
raise TaskFailedError(f"Task {self.task_id} was cancelled") from e
|
|
129
|
+
|
|
130
|
+
def _resolve(self, value: Any) -> None:
|
|
131
|
+
"""Resolve the future with a result value. Called by the task framework."""
|
|
132
|
+
self._status = "complete"
|
|
133
|
+
if not self._result_future.done():
|
|
134
|
+
self._result_future.set_result(value)
|
|
135
|
+
|
|
136
|
+
def _fail(self, exc: BaseException | None = None) -> None:
|
|
137
|
+
"""Mark the task as failed. Called by the task framework."""
|
|
138
|
+
self._status = "failed"
|
|
139
|
+
if not self._result_future.done():
|
|
140
|
+
if exc is not None:
|
|
141
|
+
self._result_future.set_exception(exc)
|
|
142
|
+
else:
|
|
143
|
+
self._result_future.cancel()
|
|
144
|
+
|
|
145
|
+
async def cancel(self) -> None:
|
|
146
|
+
"""Cancel this task handle.
|
|
147
|
+
|
|
148
|
+
Propagates a TASK_CANCEL signal via the bound gateway when one
|
|
149
|
+
is set (T1-05). Local-only handles flip status without any
|
|
150
|
+
network traffic.
|
|
151
|
+
"""
|
|
152
|
+
if self._status in ("complete", "failed"):
|
|
153
|
+
return
|
|
154
|
+
# Backwards-compat: cancel transitions to "failed" (v0.x contract).
|
|
155
|
+
self._status = "failed"
|
|
156
|
+
if not self._result_future.done():
|
|
157
|
+
self._result_future.cancel()
|
|
158
|
+
if self._gateway is not None:
|
|
159
|
+
try:
|
|
160
|
+
await self._gateway.cancel(self)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TaskTimeoutError(Exception):
|
|
166
|
+
"""Raised when a task result is not received within the timeout."""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TaskFailedError(Exception):
|
|
170
|
+
"""Raised when a task fails or is cancelled."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Gateway Protocol + implementations (T1-04)
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TaskGateway(Protocol):
|
|
179
|
+
"""Dispatch interface for :func:`submit_task`.
|
|
180
|
+
|
|
181
|
+
Implementations turn a :class:`TaskDescriptor` into a
|
|
182
|
+
:class:`TaskHandle` whose future is fulfilled when the task
|
|
183
|
+
completes (locally or remotely). Cancellation is propagated
|
|
184
|
+
through :meth:`cancel`.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
async def dispatch(self, descriptor: TaskDescriptor) -> TaskHandle: ...
|
|
188
|
+
|
|
189
|
+
async def cancel(self, handle: TaskHandle) -> None: ...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class InProcessGateway:
|
|
193
|
+
"""Default gateway — returns a bare :class:`TaskHandle`.
|
|
194
|
+
|
|
195
|
+
The handle is fulfilled by whoever owns the in-process bus / agent
|
|
196
|
+
plumbing (this module does not auto-resolve; v0.x callers were
|
|
197
|
+
already wiring resolution themselves).
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
async def dispatch(self, descriptor: TaskDescriptor) -> TaskHandle:
|
|
201
|
+
handle = TaskHandle(
|
|
202
|
+
task_id=descriptor.task_id,
|
|
203
|
+
submitter_kid=descriptor.submitter_kid,
|
|
204
|
+
assignee_kid=descriptor.assignee_kid,
|
|
205
|
+
topic=descriptor.topic,
|
|
206
|
+
_gateway=self,
|
|
207
|
+
_request_kid=descriptor.request_kid,
|
|
208
|
+
)
|
|
209
|
+
return handle
|
|
210
|
+
|
|
211
|
+
async def cancel(self, handle: TaskHandle) -> None:
|
|
212
|
+
# Local handle — nothing to propagate beyond the future cancellation
|
|
213
|
+
# already performed inside TaskHandle.cancel().
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class MCPGateway:
|
|
218
|
+
"""Dispatch :func:`submit_task` over an :class:`MCPClient`.
|
|
219
|
+
|
|
220
|
+
The remote server MUST expose two tools:
|
|
221
|
+
|
|
222
|
+
- ``submit_task`` — accepts the serialised descriptor, returns a
|
|
223
|
+
``{"task_id": ..., "result": ...}`` envelope on completion.
|
|
224
|
+
- ``cancel_task`` — accepts ``{"task_id": ...}``.
|
|
225
|
+
|
|
226
|
+
Idempotency: if the same descriptor (== same ``request_kid``) is
|
|
227
|
+
dispatched twice, the second call returns the cached
|
|
228
|
+
:class:`TaskHandle` from the first dispatch (T1-05).
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
client: MCPClient,
|
|
234
|
+
*,
|
|
235
|
+
signing_keypair: Keypair,
|
|
236
|
+
cap_token_root: bytes,
|
|
237
|
+
) -> None:
|
|
238
|
+
self._client = client
|
|
239
|
+
self._keypair = signing_keypair
|
|
240
|
+
self._cap_token_root = cap_token_root
|
|
241
|
+
self._pending: dict[bytes, TaskHandle] = {}
|
|
242
|
+
self._tasks: dict[bytes, asyncio.Task] = {}
|
|
243
|
+
|
|
244
|
+
async def dispatch(self, descriptor: TaskDescriptor) -> TaskHandle:
|
|
245
|
+
request_kid = descriptor.request_kid
|
|
246
|
+
# Idempotent retry: same descriptor → same handle.
|
|
247
|
+
if request_kid in self._pending:
|
|
248
|
+
return self._pending[request_kid]
|
|
249
|
+
|
|
250
|
+
handle = TaskHandle(
|
|
251
|
+
task_id=descriptor.task_id,
|
|
252
|
+
submitter_kid=descriptor.submitter_kid,
|
|
253
|
+
assignee_kid=descriptor.assignee_kid,
|
|
254
|
+
topic=descriptor.topic,
|
|
255
|
+
_gateway=self,
|
|
256
|
+
_request_kid=request_kid,
|
|
257
|
+
)
|
|
258
|
+
self._pending[request_kid] = handle
|
|
259
|
+
|
|
260
|
+
async def _run() -> None:
|
|
261
|
+
try:
|
|
262
|
+
envelope = await self._client.call_tool(
|
|
263
|
+
"submit_task",
|
|
264
|
+
{
|
|
265
|
+
"task_id": descriptor.task_id,
|
|
266
|
+
"submitter_kid": descriptor.submitter_kid.hex(),
|
|
267
|
+
"assignee_kid": descriptor.assignee_kid.hex(),
|
|
268
|
+
"topic": descriptor.topic,
|
|
269
|
+
"arguments": descriptor.arguments,
|
|
270
|
+
},
|
|
271
|
+
signing_keypair=self._keypair,
|
|
272
|
+
cap_token_root=self._cap_token_root,
|
|
273
|
+
)
|
|
274
|
+
content = envelope.get("content") if isinstance(envelope, dict) else envelope
|
|
275
|
+
handle._resolve(content)
|
|
276
|
+
except asyncio.CancelledError:
|
|
277
|
+
handle._fail(TaskFailedError("task cancelled"))
|
|
278
|
+
raise
|
|
279
|
+
except Exception as e:
|
|
280
|
+
handle._fail(e)
|
|
281
|
+
|
|
282
|
+
self._tasks[request_kid] = asyncio.create_task(_run())
|
|
283
|
+
return handle
|
|
284
|
+
|
|
285
|
+
async def cancel(self, handle: TaskHandle) -> None:
|
|
286
|
+
kid = handle.request_kid
|
|
287
|
+
task = self._tasks.pop(kid, None)
|
|
288
|
+
if task is not None and not task.done():
|
|
289
|
+
task.cancel()
|
|
290
|
+
# Best-effort remote cancel — ignore errors, the remote side
|
|
291
|
+
# may have already completed or never seen the task.
|
|
292
|
+
try:
|
|
293
|
+
await self._client.call_tool(
|
|
294
|
+
"cancel_task",
|
|
295
|
+
{"task_id": handle.task_id},
|
|
296
|
+
signing_keypair=self._keypair,
|
|
297
|
+
cap_token_root=self._cap_token_root,
|
|
298
|
+
)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# submit_task entry point
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def submit_task(
|
|
309
|
+
*,
|
|
310
|
+
submitter_kid: bytes,
|
|
311
|
+
assignee_kid: bytes,
|
|
312
|
+
topic: str,
|
|
313
|
+
arguments: dict[str, Any] | None = None,
|
|
314
|
+
timeout_s: float = 30.0,
|
|
315
|
+
gateway: TaskGateway | None = None,
|
|
316
|
+
task_id: str | None = None,
|
|
317
|
+
) -> TaskHandle:
|
|
318
|
+
"""Submit a task to a :class:`TaskGateway`.
|
|
319
|
+
|
|
320
|
+
Defaults to :class:`InProcessGateway` to preserve v0.x semantics
|
|
321
|
+
(callers that already wire resolution into the in-process bus
|
|
322
|
+
continue to work unchanged). Pass a :class:`MCPGateway` to
|
|
323
|
+
dispatch over a network transport (T1-04).
|
|
324
|
+
"""
|
|
325
|
+
descriptor = TaskDescriptor(
|
|
326
|
+
task_id=task_id or secrets.token_hex(8),
|
|
327
|
+
submitter_kid=submitter_kid,
|
|
328
|
+
assignee_kid=assignee_kid,
|
|
329
|
+
topic=topic,
|
|
330
|
+
arguments=arguments or {},
|
|
331
|
+
timeout_s=timeout_s,
|
|
332
|
+
)
|
|
333
|
+
gw = gateway if gateway is not None else InProcessGateway()
|
|
334
|
+
return await gw.dispatch(descriptor)
|