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/mcp/server.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""MCP server — JSON-RPC 2.0 dispatcher with CapToken-bound provenance.
|
|
2
|
+
|
|
3
|
+
Accepts a :class:`MCPTransport` and a tool registry view, dispatches
|
|
4
|
+
``initialize`` / ``tools/list`` / ``tools/call`` requests, and enforces
|
|
5
|
+
the T1-03 provenance binding before invoking any tool handler.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from hypermind.mcp.provenance import (
|
|
17
|
+
ERR_PROVENANCE_MISSING,
|
|
18
|
+
ERR_PROVENANCE_UNKNOWN_CAP,
|
|
19
|
+
Provenance,
|
|
20
|
+
ProvenanceError,
|
|
21
|
+
compute_cap_token_root,
|
|
22
|
+
verify_provenance,
|
|
23
|
+
)
|
|
24
|
+
from hypermind.mcp.transport import MCP_SCHEMA_VERSION, MCPTransport
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# JSON-RPC 2.0 standard error codes (§5.1).
|
|
30
|
+
ERR_PARSE = -32700
|
|
31
|
+
ERR_INVALID_REQUEST = -32600
|
|
32
|
+
ERR_METHOD_NOT_FOUND = -32601
|
|
33
|
+
ERR_INVALID_PARAMS = -32602
|
|
34
|
+
ERR_INTERNAL = -32603
|
|
35
|
+
|
|
36
|
+
#: Async handler signature: ``async def handler(params) -> result``.
|
|
37
|
+
ToolHandler = Callable[[dict[str, Any]], Awaitable[Any]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ToolDescriptor:
|
|
42
|
+
"""An MCP-visible tool entry."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
description: str
|
|
46
|
+
parameters_schema: dict[str, Any]
|
|
47
|
+
handler: ToolHandler
|
|
48
|
+
|
|
49
|
+
def to_mcp_dict(self) -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"description": self.description,
|
|
53
|
+
"inputSchema": self.parameters_schema,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class MCPServer:
|
|
59
|
+
"""JSON-RPC 2.0 dispatcher serving a tool registry over an
|
|
60
|
+
:class:`MCPTransport`.
|
|
61
|
+
|
|
62
|
+
Provenance enforcement
|
|
63
|
+
----------------------
|
|
64
|
+
Every ``tools/call`` request MUST carry a ``provenance`` block in
|
|
65
|
+
its ``params`` (signed Ed25519 over the canonical CBOR of the
|
|
66
|
+
request body — see :mod:`hypermind.mcp.provenance`). The server
|
|
67
|
+
rejects calls that:
|
|
68
|
+
|
|
69
|
+
1. Omit the provenance block (``-32001``).
|
|
70
|
+
2. Reference a ``cap_token_root`` not in :attr:`known_cap_roots`
|
|
71
|
+
(``-32003``).
|
|
72
|
+
3. Have an invalid signature or mismatched ``request_kid``
|
|
73
|
+
(``-32002`` / ``-32004``).
|
|
74
|
+
|
|
75
|
+
Idempotency
|
|
76
|
+
-----------
|
|
77
|
+
A successful ``tools/call`` caches its result keyed by
|
|
78
|
+
``request_kid``. Re-receiving the same ``request_kid`` returns the
|
|
79
|
+
cached result without re-dispatching the handler — closing T1-05's
|
|
80
|
+
"idempotent retry" requirement.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
tools: dict[str, ToolDescriptor] = field(default_factory=dict)
|
|
84
|
+
#: SHA-256 hashes of root CapToken canonical bytes that are
|
|
85
|
+
#: authorised to call tools on this server. The server checks
|
|
86
|
+
#: membership only — full CapToken validation happens upstream
|
|
87
|
+
#: when the cap is registered.
|
|
88
|
+
known_cap_roots: set[bytes] = field(default_factory=set)
|
|
89
|
+
#: request_kid -> cached result (for idempotent retries).
|
|
90
|
+
_idempotency_cache: dict[bytes, Any] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
def register_tool(
|
|
93
|
+
self,
|
|
94
|
+
name: str,
|
|
95
|
+
*,
|
|
96
|
+
description: str,
|
|
97
|
+
parameters_schema: dict[str, Any],
|
|
98
|
+
handler: ToolHandler,
|
|
99
|
+
) -> None:
|
|
100
|
+
if name in self.tools:
|
|
101
|
+
raise ValueError(f"tool {name!r} already registered")
|
|
102
|
+
self.tools[name] = ToolDescriptor(
|
|
103
|
+
name=name,
|
|
104
|
+
description=description,
|
|
105
|
+
parameters_schema=parameters_schema,
|
|
106
|
+
handler=handler,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def register_cap_root(self, cap_token_bytes: bytes) -> bytes:
|
|
110
|
+
"""Register a CapToken root and return its SHA-256 root hash."""
|
|
111
|
+
root = compute_cap_token_root(cap_token_bytes)
|
|
112
|
+
self.known_cap_roots.add(root)
|
|
113
|
+
return root
|
|
114
|
+
|
|
115
|
+
# ---- request loop --------------------------------------------------
|
|
116
|
+
|
|
117
|
+
async def serve(self, transport: MCPTransport) -> None:
|
|
118
|
+
"""Service requests until the transport is closed."""
|
|
119
|
+
try:
|
|
120
|
+
while True:
|
|
121
|
+
try:
|
|
122
|
+
msg = await transport.recv()
|
|
123
|
+
except (ConnectionError, asyncio.CancelledError):
|
|
124
|
+
return
|
|
125
|
+
except (ValueError, Exception) as e:
|
|
126
|
+
logger.warning("MCP recv error: %s", e)
|
|
127
|
+
await self._send_error(transport, None, ERR_PARSE, str(e))
|
|
128
|
+
continue
|
|
129
|
+
await self._dispatch(transport, msg)
|
|
130
|
+
finally:
|
|
131
|
+
await transport.close()
|
|
132
|
+
|
|
133
|
+
async def _dispatch(self, transport: MCPTransport, msg: dict[str, Any]) -> None:
|
|
134
|
+
if msg.get("jsonrpc") != "2.0":
|
|
135
|
+
await self._send_error(
|
|
136
|
+
transport,
|
|
137
|
+
msg.get("id"),
|
|
138
|
+
ERR_INVALID_REQUEST,
|
|
139
|
+
"jsonrpc field must be '2.0'",
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
msg_id = msg.get("id")
|
|
143
|
+
method = msg.get("method")
|
|
144
|
+
params = msg.get("params") or {}
|
|
145
|
+
if not isinstance(method, str):
|
|
146
|
+
await self._send_error(
|
|
147
|
+
transport, msg_id, ERR_INVALID_REQUEST, "method must be a string"
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
if not isinstance(params, dict):
|
|
151
|
+
await self._send_error(
|
|
152
|
+
transport, msg_id, ERR_INVALID_PARAMS, "params must be an object"
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
try:
|
|
156
|
+
result = await self._handle(method, params)
|
|
157
|
+
except ProvenanceError as e:
|
|
158
|
+
await self._send_error(transport, msg_id, e.code, e.message)
|
|
159
|
+
return
|
|
160
|
+
except _MethodNotFound as e:
|
|
161
|
+
await self._send_error(transport, msg_id, ERR_METHOD_NOT_FOUND, str(e))
|
|
162
|
+
return
|
|
163
|
+
except _InvalidParams as e:
|
|
164
|
+
await self._send_error(transport, msg_id, ERR_INVALID_PARAMS, str(e))
|
|
165
|
+
return
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.exception("MCP handler raised")
|
|
168
|
+
await self._send_error(transport, msg_id, ERR_INTERNAL, str(e))
|
|
169
|
+
return
|
|
170
|
+
await transport.send({"jsonrpc": "2.0", "id": msg_id, "result": result})
|
|
171
|
+
|
|
172
|
+
async def _handle(self, method: str, params: dict[str, Any]) -> Any:
|
|
173
|
+
if method == "initialize":
|
|
174
|
+
return {
|
|
175
|
+
"protocolVersion": MCP_SCHEMA_VERSION,
|
|
176
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
177
|
+
"serverInfo": {"name": "hypermind-mcp", "version": "0.10.1"},
|
|
178
|
+
}
|
|
179
|
+
if method == "tools/list":
|
|
180
|
+
return {"tools": [t.to_mcp_dict() for t in self.tools.values()]}
|
|
181
|
+
if method == "tools/call":
|
|
182
|
+
return await self._handle_tool_call(params)
|
|
183
|
+
raise _MethodNotFound(f"unknown method {method!r}")
|
|
184
|
+
|
|
185
|
+
async def _handle_tool_call(self, params: dict[str, Any]) -> Any:
|
|
186
|
+
name = params.get("name")
|
|
187
|
+
if not isinstance(name, str):
|
|
188
|
+
raise _InvalidParams("tools/call requires 'name' (string)")
|
|
189
|
+
arguments = params.get("arguments") or {}
|
|
190
|
+
if not isinstance(arguments, dict):
|
|
191
|
+
raise _InvalidParams("tools/call 'arguments' must be an object")
|
|
192
|
+
tool = self.tools.get(name)
|
|
193
|
+
if tool is None:
|
|
194
|
+
raise _MethodNotFound(f"unknown tool {name!r}")
|
|
195
|
+
|
|
196
|
+
# ---- T1-03: provenance enforcement -----------------------------
|
|
197
|
+
prov_obj = params.get("provenance")
|
|
198
|
+
if prov_obj is None:
|
|
199
|
+
raise ProvenanceError(
|
|
200
|
+
ERR_PROVENANCE_MISSING,
|
|
201
|
+
"tools/call requires a 'provenance' object (T1-03)",
|
|
202
|
+
)
|
|
203
|
+
provenance = Provenance.from_dict(prov_obj)
|
|
204
|
+
# The signed body is the params *without* provenance — strip it
|
|
205
|
+
# before recomputing the request_kid.
|
|
206
|
+
signed_params = {k: v for k, v in params.items() if k != "provenance"}
|
|
207
|
+
verify_provenance(
|
|
208
|
+
method="tools/call",
|
|
209
|
+
params_without_provenance=signed_params,
|
|
210
|
+
provenance=provenance,
|
|
211
|
+
)
|
|
212
|
+
if provenance.cap_token_root not in self.known_cap_roots:
|
|
213
|
+
raise ProvenanceError(
|
|
214
|
+
ERR_PROVENANCE_UNKNOWN_CAP,
|
|
215
|
+
"cap_token_root is not registered with this server",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# ---- T1-05: idempotent retry -----------------------------------
|
|
219
|
+
cached = self._idempotency_cache.get(provenance.request_kid)
|
|
220
|
+
if cached is not None:
|
|
221
|
+
return cached
|
|
222
|
+
result = await tool.handler(arguments)
|
|
223
|
+
envelope = {"content": result, "request_kid": provenance.request_kid.hex()}
|
|
224
|
+
self._idempotency_cache[provenance.request_kid] = envelope
|
|
225
|
+
return envelope
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
async def _send_error(
|
|
229
|
+
transport: MCPTransport,
|
|
230
|
+
msg_id: Any,
|
|
231
|
+
code: int,
|
|
232
|
+
message: str,
|
|
233
|
+
) -> None:
|
|
234
|
+
await transport.send(
|
|
235
|
+
{
|
|
236
|
+
"jsonrpc": "2.0",
|
|
237
|
+
"id": msg_id,
|
|
238
|
+
"error": {"code": code, "message": message},
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class _MethodNotFound(Exception):
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class _InvalidParams(Exception):
|
|
248
|
+
pass
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""MCP transport layer — JSON-RPC 2.0 over stdio (and WebSocket, deferred).
|
|
2
|
+
|
|
3
|
+
Pinned MCP schema version: **MCP v1.0** (`MCP_SCHEMA_VERSION` constant
|
|
4
|
+
below). The JSON-RPC 2.0 envelope on the wire is::
|
|
5
|
+
|
|
6
|
+
{"jsonrpc": "2.0", "id": <int|str>, "method": <str>, "params": <obj>}
|
|
7
|
+
|
|
8
|
+
Replies::
|
|
9
|
+
|
|
10
|
+
{"jsonrpc": "2.0", "id": <id>, "result": <obj>}
|
|
11
|
+
{"jsonrpc": "2.0", "id": <id>, "error": {"code": <int>, "message": <str>}}
|
|
12
|
+
|
|
13
|
+
Two transports ship in v0.10.1:
|
|
14
|
+
|
|
15
|
+
- :class:`StdioTransport` — line-delimited JSON over a pair of byte
|
|
16
|
+
streams (asyncio :class:`asyncio.StreamReader`/:class:`StreamWriter`).
|
|
17
|
+
Suitable for in-process pipes, subprocess stdin/stdout, and Unix
|
|
18
|
+
domain sockets wrapped as streams.
|
|
19
|
+
- :class:`WebSocketTransport` — only available when the optional
|
|
20
|
+
``websockets`` extra is installed. v0.10.1 ships the class but
|
|
21
|
+
declares WebSocket support as deferred to v0.10.2 if the dependency
|
|
22
|
+
is not present at import time. The class itself imports lazily.
|
|
23
|
+
|
|
24
|
+
Both transports implement the :class:`MCPTransport` Protocol.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
from typing import Any, Protocol, runtime_checkable
|
|
32
|
+
|
|
33
|
+
#: MCP schema version this transport speaks. Bound into the
|
|
34
|
+
#: ``initialize`` handshake so peers can negotiate.
|
|
35
|
+
MCP_SCHEMA_VERSION = "1.0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class MCPTransport(Protocol):
|
|
40
|
+
"""Bidirectional message channel for JSON-RPC 2.0 frames."""
|
|
41
|
+
|
|
42
|
+
async def send(self, msg: dict[str, Any]) -> None: ...
|
|
43
|
+
|
|
44
|
+
async def recv(self) -> dict[str, Any]: ...
|
|
45
|
+
|
|
46
|
+
async def close(self) -> None: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StdioTransport:
|
|
50
|
+
"""Line-delimited JSON over an asyncio reader/writer pair.
|
|
51
|
+
|
|
52
|
+
Each frame is a single JSON object terminated by ``\\n``. Empty
|
|
53
|
+
lines and lines that fail to decode are surfaced as
|
|
54
|
+
:class:`ValueError` from :meth:`recv` so the caller can decide
|
|
55
|
+
whether to skip or close the channel.
|
|
56
|
+
|
|
57
|
+
The transport accepts either a real :class:`asyncio.StreamWriter`
|
|
58
|
+
or a duck-typed object that exposes ``write(bytes)`` plus async
|
|
59
|
+
``drain()`` / ``close()``. The in-memory test pair below uses the
|
|
60
|
+
duck-typed form to avoid the StreamReaderProtocol handshake that
|
|
61
|
+
asyncio's real StreamWriter needs.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
reader: asyncio.StreamReader,
|
|
67
|
+
writer: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._reader = reader
|
|
70
|
+
self._writer = writer
|
|
71
|
+
self._send_lock = asyncio.Lock()
|
|
72
|
+
self._closed = False
|
|
73
|
+
|
|
74
|
+
async def send(self, msg: dict[str, Any]) -> None:
|
|
75
|
+
if self._closed:
|
|
76
|
+
raise ConnectionError("transport closed")
|
|
77
|
+
line = (json.dumps(msg, separators=(",", ":")) + "\n").encode("utf-8")
|
|
78
|
+
async with self._send_lock:
|
|
79
|
+
self._writer.write(line)
|
|
80
|
+
drain = getattr(self._writer, "drain", None)
|
|
81
|
+
if drain is not None:
|
|
82
|
+
result = drain()
|
|
83
|
+
if asyncio.iscoroutine(result):
|
|
84
|
+
await result
|
|
85
|
+
|
|
86
|
+
async def recv(self) -> dict[str, Any]:
|
|
87
|
+
if self._closed:
|
|
88
|
+
raise ConnectionError("transport closed")
|
|
89
|
+
line = await self._reader.readline()
|
|
90
|
+
if not line:
|
|
91
|
+
raise ConnectionError("peer closed transport")
|
|
92
|
+
try:
|
|
93
|
+
obj = json.loads(line.decode("utf-8").rstrip("\n"))
|
|
94
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
95
|
+
raise ValueError(f"non-JSON frame: {e}") from e
|
|
96
|
+
if not isinstance(obj, dict):
|
|
97
|
+
raise ValueError("JSON-RPC frame must be a JSON object")
|
|
98
|
+
return obj
|
|
99
|
+
|
|
100
|
+
async def close(self) -> None:
|
|
101
|
+
if self._closed:
|
|
102
|
+
return
|
|
103
|
+
self._closed = True
|
|
104
|
+
try:
|
|
105
|
+
close = getattr(self._writer, "close", None)
|
|
106
|
+
if close is not None:
|
|
107
|
+
result = close()
|
|
108
|
+
if asyncio.iscoroutine(result):
|
|
109
|
+
await result
|
|
110
|
+
wait_closed = getattr(self._writer, "wait_closed", None)
|
|
111
|
+
if wait_closed is not None:
|
|
112
|
+
result = wait_closed()
|
|
113
|
+
if asyncio.iscoroutine(result):
|
|
114
|
+
await result
|
|
115
|
+
except (ConnectionError, OSError):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def in_memory_pair(cls) -> tuple[StdioTransport, StdioTransport]:
|
|
120
|
+
"""Return two transports connected by a pair of in-memory pipes.
|
|
121
|
+
|
|
122
|
+
Useful for unit tests that want JSON-RPC over the real
|
|
123
|
+
:class:`StdioTransport` code path without spawning a subprocess.
|
|
124
|
+
"""
|
|
125
|
+
a_reader: asyncio.StreamReader = asyncio.StreamReader()
|
|
126
|
+
b_reader: asyncio.StreamReader = asyncio.StreamReader()
|
|
127
|
+
# Side A reads from a_reader, writes feed b_reader (B's input).
|
|
128
|
+
side_a = cls(a_reader, _ReaderFeederWriter(b_reader))
|
|
129
|
+
side_b = cls(b_reader, _ReaderFeederWriter(a_reader))
|
|
130
|
+
return side_a, side_b
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _ReaderFeederWriter:
|
|
134
|
+
"""Tiny duck-typed writer that pushes bytes into an asyncio
|
|
135
|
+
StreamReader on the peer side. Used by
|
|
136
|
+
:meth:`StdioTransport.in_memory_pair` to wire two transports
|
|
137
|
+
without spinning up a real protocol/transport pair."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, peer_reader: asyncio.StreamReader) -> None:
|
|
140
|
+
self._peer_reader = peer_reader
|
|
141
|
+
self._closed = False
|
|
142
|
+
|
|
143
|
+
def write(self, data: bytes) -> None:
|
|
144
|
+
if self._closed:
|
|
145
|
+
raise ConnectionError("writer closed")
|
|
146
|
+
self._peer_reader.feed_data(data)
|
|
147
|
+
|
|
148
|
+
async def drain(self) -> None:
|
|
149
|
+
# In-memory: no buffer pressure, drain is a no-op.
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
if self._closed:
|
|
154
|
+
return
|
|
155
|
+
self._closed = True
|
|
156
|
+
self._peer_reader.feed_eof()
|
|
157
|
+
|
|
158
|
+
async def wait_closed(self) -> None:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# WebSocket transport — optional, deferred to v0.10.2 unless extra installed.
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class WebSocketTransport:
|
|
168
|
+
"""JSON-RPC 2.0 over a single WebSocket connection.
|
|
169
|
+
|
|
170
|
+
Requires the optional ``websockets`` dependency. v0.10.1 declares
|
|
171
|
+
this transport as a deferred capability — the class is importable
|
|
172
|
+
even without ``websockets`` installed, but instantiation raises
|
|
173
|
+
``RuntimeError`` directing the caller to install the extra. The
|
|
174
|
+
full WebSocket implementation lands in v0.10.2.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, ws: Any) -> None:
|
|
178
|
+
try:
|
|
179
|
+
import websockets # noqa: F401
|
|
180
|
+
except ImportError as e: # pragma: no cover — env-dependent
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
"WebSocketTransport requires the `websockets` package "
|
|
183
|
+
"(deferred to v0.10.2; install via `pip install websockets`)"
|
|
184
|
+
) from e
|
|
185
|
+
self._ws = ws
|
|
186
|
+
self._closed = False
|
|
187
|
+
|
|
188
|
+
async def send(self, msg: dict[str, Any]) -> None: # pragma: no cover
|
|
189
|
+
if self._closed:
|
|
190
|
+
raise ConnectionError("transport closed")
|
|
191
|
+
await self._ws.send(json.dumps(msg, separators=(",", ":")))
|
|
192
|
+
|
|
193
|
+
async def recv(self) -> dict[str, Any]: # pragma: no cover
|
|
194
|
+
raw = await self._ws.recv()
|
|
195
|
+
if isinstance(raw, bytes):
|
|
196
|
+
raw = raw.decode("utf-8")
|
|
197
|
+
obj = json.loads(raw)
|
|
198
|
+
if not isinstance(obj, dict):
|
|
199
|
+
raise ValueError("JSON-RPC frame must be a JSON object")
|
|
200
|
+
return obj
|
|
201
|
+
|
|
202
|
+
async def close(self) -> None: # pragma: no cover
|
|
203
|
+
if self._closed:
|
|
204
|
+
return
|
|
205
|
+
self._closed = True
|
|
206
|
+
await self._ws.close()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""hypermind.mind — the agent mind.
|
|
2
|
+
|
|
3
|
+
Adds a deliberation layer on top of HyperMindAgent's primitive verbs
|
|
4
|
+
(publish_claim, dispute, consult, synthesize, checkpoint). The mind is
|
|
5
|
+
strictly *additive*: agents constructed without a Deliberator behave
|
|
6
|
+
exactly as before. Calling ``agent.pursue(goal)`` activates the mind.
|
|
7
|
+
|
|
8
|
+
Public surface:
|
|
9
|
+
Goal, GoalQueue — what the agent wants to achieve
|
|
10
|
+
ActionPolicy + built-in policies — how the agent decides
|
|
11
|
+
Deliberator — the loop that turns goals into actions
|
|
12
|
+
SelfReflector — adjusts policy thresholds from track record
|
|
13
|
+
DiversityGuard — blocks correlated synthesis
|
|
14
|
+
SybilGuard — caps influence of new agents
|
|
15
|
+
LeverageCap — bounds claim impact by historical Brier
|
|
16
|
+
|
|
17
|
+
Every action dispatched by a Deliberator is gated by a CapToken (caveat
|
|
18
|
+
chain) — autonomy is governable, not unbounded.
|
|
19
|
+
|
|
20
|
+
Spec: docs/spec/19-autonomy-contract.md
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from hypermind.mind.active import (
|
|
26
|
+
AgentSnapshot,
|
|
27
|
+
AntiHerdingTrigger,
|
|
28
|
+
CandidateQuestion,
|
|
29
|
+
CurriculumScheduler,
|
|
30
|
+
GradedQuestion,
|
|
31
|
+
InformationGainPlanner,
|
|
32
|
+
)
|
|
33
|
+
from hypermind.mind.belief import (
|
|
34
|
+
BeliefRecord,
|
|
35
|
+
BeliefRevisionError,
|
|
36
|
+
BeliefStore,
|
|
37
|
+
TheoryOfMind,
|
|
38
|
+
)
|
|
39
|
+
from hypermind.mind.deliberator import Deliberator, DeliberatorTick
|
|
40
|
+
from hypermind.mind.diversity import DiversityGuard, EpistemicDependence, QuorumResult
|
|
41
|
+
from hypermind.mind.evolve import BrierFeedback, MentorFollowing, SelfReflector
|
|
42
|
+
from hypermind.mind.federation import (
|
|
43
|
+
FederationPolicy,
|
|
44
|
+
assert_never_merge,
|
|
45
|
+
auto_relay,
|
|
46
|
+
)
|
|
47
|
+
from hypermind.mind.goals import Goal, GoalOutcome, GoalQueue, GoalStatus
|
|
48
|
+
from hypermind.mind.mediator import MediatorSelector, select_mediator
|
|
49
|
+
from hypermind.mind.policy import (
|
|
50
|
+
ActionDecision,
|
|
51
|
+
ActionKind,
|
|
52
|
+
ActionPolicy,
|
|
53
|
+
CheckpointOnSignalGain,
|
|
54
|
+
ConsultOnDisagreement,
|
|
55
|
+
DisputeOnContradiction,
|
|
56
|
+
LeverageCap,
|
|
57
|
+
NoOpPolicy,
|
|
58
|
+
PolicyContext,
|
|
59
|
+
PublishWhenConfident,
|
|
60
|
+
select_top_decision,
|
|
61
|
+
)
|
|
62
|
+
from hypermind.mind.records import (
|
|
63
|
+
DissentRecord,
|
|
64
|
+
IntentRecord,
|
|
65
|
+
TraceRecord,
|
|
66
|
+
decode_mind_record,
|
|
67
|
+
encode_mind_record,
|
|
68
|
+
)
|
|
69
|
+
from hypermind.mind.roles import (
|
|
70
|
+
Role,
|
|
71
|
+
RoleKind,
|
|
72
|
+
SwarmComposition,
|
|
73
|
+
generalist,
|
|
74
|
+
mediator,
|
|
75
|
+
oracle,
|
|
76
|
+
role_by_name,
|
|
77
|
+
sceptic,
|
|
78
|
+
scout,
|
|
79
|
+
synthesist,
|
|
80
|
+
)
|
|
81
|
+
from hypermind.mind.sybil_guard import SybilGuard
|
|
82
|
+
from hypermind.mind.world_model import (
|
|
83
|
+
BetaWorldModel,
|
|
84
|
+
ImaginedOutcome,
|
|
85
|
+
LookaheadPolicy,
|
|
86
|
+
WorldModel,
|
|
87
|
+
update_world_model_from_resolution,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"ActionDecision",
|
|
92
|
+
"ActionKind",
|
|
93
|
+
"ActionPolicy",
|
|
94
|
+
"AgentSnapshot",
|
|
95
|
+
"AntiHerdingTrigger",
|
|
96
|
+
"BeliefRecord",
|
|
97
|
+
"BeliefRevisionError",
|
|
98
|
+
"BeliefStore",
|
|
99
|
+
"BetaWorldModel",
|
|
100
|
+
"BrierFeedback",
|
|
101
|
+
"CandidateQuestion",
|
|
102
|
+
"CheckpointOnSignalGain",
|
|
103
|
+
"ConsultOnDisagreement",
|
|
104
|
+
"CurriculumScheduler",
|
|
105
|
+
"Deliberator",
|
|
106
|
+
"DeliberatorTick",
|
|
107
|
+
"DisputeOnContradiction",
|
|
108
|
+
"DissentRecord",
|
|
109
|
+
"DiversityGuard",
|
|
110
|
+
"EpistemicDependence",
|
|
111
|
+
"FederationPolicy",
|
|
112
|
+
"Goal",
|
|
113
|
+
"GoalOutcome",
|
|
114
|
+
"GoalQueue",
|
|
115
|
+
"GoalStatus",
|
|
116
|
+
"GradedQuestion",
|
|
117
|
+
"ImaginedOutcome",
|
|
118
|
+
"InformationGainPlanner",
|
|
119
|
+
"IntentRecord",
|
|
120
|
+
"LeverageCap",
|
|
121
|
+
"LookaheadPolicy",
|
|
122
|
+
"MediatorSelector",
|
|
123
|
+
"MentorFollowing",
|
|
124
|
+
"NoOpPolicy",
|
|
125
|
+
"PolicyContext",
|
|
126
|
+
"PublishWhenConfident",
|
|
127
|
+
"QuorumResult",
|
|
128
|
+
"Role",
|
|
129
|
+
"RoleKind",
|
|
130
|
+
"SelfReflector",
|
|
131
|
+
"SwarmComposition",
|
|
132
|
+
"SybilGuard",
|
|
133
|
+
"TheoryOfMind",
|
|
134
|
+
"TraceRecord",
|
|
135
|
+
"WorldModel",
|
|
136
|
+
"assert_never_merge",
|
|
137
|
+
"auto_relay",
|
|
138
|
+
"decode_mind_record",
|
|
139
|
+
"encode_mind_record",
|
|
140
|
+
"generalist",
|
|
141
|
+
"mediator",
|
|
142
|
+
"oracle",
|
|
143
|
+
"role_by_name",
|
|
144
|
+
"sceptic",
|
|
145
|
+
"scout",
|
|
146
|
+
"select_mediator",
|
|
147
|
+
"select_top_decision",
|
|
148
|
+
"synthesist",
|
|
149
|
+
"update_world_model_from_resolution",
|
|
150
|
+
]
|