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/api/app.py
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
"""HTTP management API ASGI app — ``management_app(agent)``.
|
|
2
|
+
|
|
3
|
+
Surface is intentionally small: every route corresponds 1:1 to a public
|
|
4
|
+
``HyperMindAgent`` verb. No CRUD on internal state; no admin overrides.
|
|
5
|
+
The agent's existing ramp + capability gates fully apply.
|
|
6
|
+
|
|
7
|
+
Auth is out of scope for v0.5 — operators put this behind their own
|
|
8
|
+
proxy (Caddy, Envoy, Tailscale ACL) until v0.6's TLS+mTLS story lands.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Awaitable, Callable
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from hypermind.observability.asgi import asgi_app as _obs_app
|
|
20
|
+
from hypermind.uncertainty import Uncertainty
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from hypermind.agent import HyperMindAgent
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_Send = Callable[[dict[str, Any]], Awaitable[None]]
|
|
27
|
+
_Receive = Callable[[], Awaitable[dict[str, Any]]]
|
|
28
|
+
_Scope = dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --------------------------------------------------------------------------
|
|
32
|
+
# Helpers
|
|
33
|
+
# --------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _read_body(receive: _Receive) -> bytes:
|
|
37
|
+
body = b""
|
|
38
|
+
while True:
|
|
39
|
+
msg = await receive()
|
|
40
|
+
body += msg.get("body", b"")
|
|
41
|
+
if not msg.get("more_body", False):
|
|
42
|
+
break
|
|
43
|
+
return body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _send_json(send: _Send, *, status: int, payload: Any) -> None:
|
|
47
|
+
body = json.dumps(payload, default=_json_default).encode("utf-8")
|
|
48
|
+
await send(
|
|
49
|
+
{
|
|
50
|
+
"type": "http.response.start",
|
|
51
|
+
"status": status,
|
|
52
|
+
"headers": [
|
|
53
|
+
(b"content-type", b"application/json"),
|
|
54
|
+
(b"content-length", str(len(body)).encode()),
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
await send({"type": "http.response.body", "body": body, "more_body": False})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _json_default(obj: Any) -> Any:
|
|
62
|
+
if isinstance(obj, (bytes, bytearray)):
|
|
63
|
+
return obj.hex()
|
|
64
|
+
if hasattr(obj, "to_dict"):
|
|
65
|
+
return obj.to_dict()
|
|
66
|
+
if hasattr(obj, "__dict__"):
|
|
67
|
+
return obj.__dict__
|
|
68
|
+
return str(obj)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _err(send: _Send, status: int, message: str, *, rule_id: str | None = None) -> Awaitable[None]:
|
|
72
|
+
payload: dict[str, Any] = {"error": message}
|
|
73
|
+
if rule_id:
|
|
74
|
+
payload["rule_id"] = rule_id
|
|
75
|
+
return _send_json(send, status=status, payload=payload)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_kid(s: str) -> bytes:
|
|
79
|
+
s = s.strip().lower().removesuffix("…").rstrip(".")
|
|
80
|
+
return bytes.fromhex(s)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --------------------------------------------------------------------------
|
|
84
|
+
# Route handlers
|
|
85
|
+
# --------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _handle_publish(agent: HyperMindAgent, body: bytes, send: _Send) -> None:
|
|
89
|
+
try:
|
|
90
|
+
payload = json.loads(body) if body else {}
|
|
91
|
+
except json.JSONDecodeError as exc:
|
|
92
|
+
await _err(send, 400, f"invalid JSON: {exc}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
claim_payload = payload.get("payload")
|
|
96
|
+
claim_type = payload.get("claim_type", "custom")
|
|
97
|
+
topic = payload.get("topic")
|
|
98
|
+
unc_dict = payload.get("uncertainty", {})
|
|
99
|
+
|
|
100
|
+
if claim_payload is None:
|
|
101
|
+
await _err(send, 400, "missing field: payload")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Build Uncertainty from a small JSON shape
|
|
105
|
+
try:
|
|
106
|
+
unc = _uncertainty_from_dict(unc_dict)
|
|
107
|
+
except (KeyError, ValueError) as exc:
|
|
108
|
+
await _err(send, 400, f"invalid uncertainty: {exc}")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
kid = await agent.publish_claim(
|
|
113
|
+
payload=claim_payload,
|
|
114
|
+
claim_type=claim_type,
|
|
115
|
+
uncertainty=unc,
|
|
116
|
+
topic=topic,
|
|
117
|
+
)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
rule_id = getattr(exc, "rule_id", None)
|
|
120
|
+
await _err(send, 422, str(exc), rule_id=rule_id)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
await _send_json(send, status=201, payload={"kid": kid.hex()})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _uncertainty_from_dict(d: dict[str, Any]) -> Uncertainty:
|
|
127
|
+
"""Tiny JSON shape for Uncertainty.
|
|
128
|
+
|
|
129
|
+
Accepts:
|
|
130
|
+
{"kind": "beta", "alpha": 8, "beta": 2}
|
|
131
|
+
{"kind": "gauss", "mu": 0.5, "sigma2": 0.01}
|
|
132
|
+
{"kind": "unknown"}
|
|
133
|
+
{"kind": "point", "value": 0.7}
|
|
134
|
+
{"confidence": 0.8} # short form, maps to Beta(8, 2)
|
|
135
|
+
"""
|
|
136
|
+
if not d:
|
|
137
|
+
return Uncertainty.unknown()
|
|
138
|
+
kind = d.get("kind")
|
|
139
|
+
if kind == "beta":
|
|
140
|
+
return Uncertainty.beta(alpha=float(d["alpha"]), beta=float(d["beta"]))
|
|
141
|
+
if kind == "gauss":
|
|
142
|
+
return Uncertainty.gauss(mu=float(d["mu"]), sigma2=float(d["sigma2"]))
|
|
143
|
+
if kind == "unknown":
|
|
144
|
+
return Uncertainty.unknown()
|
|
145
|
+
if kind == "point":
|
|
146
|
+
return Uncertainty.point(float(d["value"]))
|
|
147
|
+
if "confidence" in d:
|
|
148
|
+
# Default short form
|
|
149
|
+
from hypermind.responders.base import confidence_to_uncertainty
|
|
150
|
+
|
|
151
|
+
return confidence_to_uncertainty(float(d["confidence"]))
|
|
152
|
+
raise ValueError(f"unknown uncertainty kind: {kind!r}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def _handle_get_claim(agent: HyperMindAgent, kid_hex: str, send: _Send) -> None:
|
|
156
|
+
if agent._world is None:
|
|
157
|
+
await _err(send, 404, "no world attached")
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
kid = _parse_kid(kid_hex)
|
|
161
|
+
except ValueError:
|
|
162
|
+
await _err(send, 400, f"invalid kid hex: {kid_hex!r}")
|
|
163
|
+
return
|
|
164
|
+
stmt = agent._world.sealed.get(kid)
|
|
165
|
+
if stmt is None:
|
|
166
|
+
await _err(send, 404, "claim not found")
|
|
167
|
+
return
|
|
168
|
+
await _send_json(
|
|
169
|
+
send,
|
|
170
|
+
status=200,
|
|
171
|
+
payload={
|
|
172
|
+
"kid": stmt.kid.hex(),
|
|
173
|
+
"issuer_kid": stmt.issuer_kid.hex(),
|
|
174
|
+
"record_type": stmt.record_type,
|
|
175
|
+
"alg": stmt.alg,
|
|
176
|
+
"hlc": str(stmt.hlc),
|
|
177
|
+
"namespace_id": stmt.namespace_id.hex(),
|
|
178
|
+
"wire_size": len(stmt.to_bytes()),
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def _handle_audit(agent: HyperMindAgent, since_hlc: str | None, send: _Send) -> None:
|
|
184
|
+
records: list[dict[str, Any]] = []
|
|
185
|
+
try:
|
|
186
|
+
async for line in agent.audit_export(since_hlc=since_hlc):
|
|
187
|
+
decoded = line.decode() if isinstance(line, bytes) else line
|
|
188
|
+
# audit_export emits "JSONL\tHEX-SIG"
|
|
189
|
+
try:
|
|
190
|
+
json_part, sig_part = decoded.split("\t", 1)
|
|
191
|
+
rec = json.loads(json_part)
|
|
192
|
+
rec["_signature"] = sig_part.strip()
|
|
193
|
+
records.append(rec)
|
|
194
|
+
except (ValueError, json.JSONDecodeError):
|
|
195
|
+
records.append({"raw": decoded})
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
await _err(send, 500, f"audit export failed: {exc}")
|
|
198
|
+
return
|
|
199
|
+
await _send_json(
|
|
200
|
+
send,
|
|
201
|
+
status=200,
|
|
202
|
+
payload={
|
|
203
|
+
"count": len(records),
|
|
204
|
+
"records": records,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def _handle_reputation(
|
|
210
|
+
agent: HyperMindAgent,
|
|
211
|
+
kid_hex: str,
|
|
212
|
+
topic: str | None,
|
|
213
|
+
send: _Send,
|
|
214
|
+
) -> None:
|
|
215
|
+
try:
|
|
216
|
+
target_kid = _parse_kid(kid_hex)
|
|
217
|
+
except ValueError:
|
|
218
|
+
await _err(send, 400, f"invalid kid hex: {kid_hex!r}")
|
|
219
|
+
return
|
|
220
|
+
try:
|
|
221
|
+
snap = await agent.reputation_of(target_kid, topic=topic or "default")
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
await _err(send, 500, str(exc))
|
|
224
|
+
return
|
|
225
|
+
await _send_json(
|
|
226
|
+
send,
|
|
227
|
+
status=200,
|
|
228
|
+
payload={
|
|
229
|
+
"agent_kid": snap.agent_id.hex(),
|
|
230
|
+
"topic": snap.topic,
|
|
231
|
+
"score": snap.score,
|
|
232
|
+
"n_observations": snap.n_observations,
|
|
233
|
+
"last_updated_ms": snap.last_updated_ms,
|
|
234
|
+
"momentum_z": snap.momentum_z,
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _handle_disputes(agent: HyperMindAgent, send: _Send) -> None:
|
|
240
|
+
if agent._world is None:
|
|
241
|
+
await _send_json(send, status=200, payload={"disputes": []})
|
|
242
|
+
return
|
|
243
|
+
items = []
|
|
244
|
+
for kid, dispute in agent._world.disputes.items():
|
|
245
|
+
items.append(
|
|
246
|
+
{
|
|
247
|
+
"claim_kid": kid.hex(),
|
|
248
|
+
"initiator": dispute.initiator.hex(),
|
|
249
|
+
"state": str(dispute.state),
|
|
250
|
+
"bond_topic_scope": dispute.bond_topic_scope,
|
|
251
|
+
"bond_amount": dispute.bond_amount,
|
|
252
|
+
"transitions": [
|
|
253
|
+
{"state": str(s), "ms": ms, "reason": reason}
|
|
254
|
+
for s, ms, reason in dispute.transitions
|
|
255
|
+
],
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
await _send_json(send, status=200, payload={"count": len(items), "disputes": items})
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def _handle_consult(agent: HyperMindAgent, body: bytes, send: _Send) -> None:
|
|
262
|
+
try:
|
|
263
|
+
payload = json.loads(body) if body else {}
|
|
264
|
+
except json.JSONDecodeError as exc:
|
|
265
|
+
await _err(send, 400, f"invalid JSON: {exc}")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
query = payload.get("query")
|
|
269
|
+
if not query:
|
|
270
|
+
await _err(send, 400, "missing field: query")
|
|
271
|
+
return
|
|
272
|
+
panel_size = int(payload.get("panel_size", 5))
|
|
273
|
+
route_strategy = payload.get("route_strategy", "disagreement")
|
|
274
|
+
topic = payload.get("topic")
|
|
275
|
+
timeout_s = float(payload.get("timeout_s", 30.0))
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
result = await agent.consult(
|
|
279
|
+
query,
|
|
280
|
+
panel_size=panel_size,
|
|
281
|
+
route_strategy=route_strategy,
|
|
282
|
+
topic=topic,
|
|
283
|
+
timeout=timedelta(seconds=timeout_s),
|
|
284
|
+
)
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
rule_id = getattr(exc, "rule_id", None)
|
|
287
|
+
await _err(send, 422, str(exc), rule_id=rule_id)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
await _send_json(
|
|
291
|
+
send,
|
|
292
|
+
status=200,
|
|
293
|
+
payload={
|
|
294
|
+
"query": query,
|
|
295
|
+
"posterior_mean": result.posterior.mean(),
|
|
296
|
+
"disagreement": result.disagreement,
|
|
297
|
+
"panel_size": len(result.panel),
|
|
298
|
+
"panel": [
|
|
299
|
+
{
|
|
300
|
+
"agent_kid": m.agent_id.hex(),
|
|
301
|
+
"weight": m.weight,
|
|
302
|
+
"posterior_mean": m.posterior.mean(),
|
|
303
|
+
}
|
|
304
|
+
for m in result.panel
|
|
305
|
+
],
|
|
306
|
+
"receipt_kid": result.receipt_kid.hex() if result.receipt_kid else None,
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def _handle_introspect(agent: HyperMindAgent, send: _Send) -> None:
|
|
312
|
+
try:
|
|
313
|
+
info = await agent.introspect()
|
|
314
|
+
except Exception as exc:
|
|
315
|
+
await _err(send, 500, str(exc))
|
|
316
|
+
return
|
|
317
|
+
await _send_json(send, status=200, payload=info)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def _handle_stats(agent: HyperMindAgent, send: _Send) -> None:
|
|
321
|
+
payload: dict[str, Any] = {
|
|
322
|
+
"agent_kid": agent.keypair.kid.hex(),
|
|
323
|
+
"did_key": agent.keypair.did_key,
|
|
324
|
+
"namespace": agent.namespace,
|
|
325
|
+
"rooms": list(agent.rooms),
|
|
326
|
+
"role": agent.role,
|
|
327
|
+
"ramp_paused": getattr(agent, "_ramp_paused", False),
|
|
328
|
+
}
|
|
329
|
+
if agent._world is not None:
|
|
330
|
+
payload["world"] = {
|
|
331
|
+
"namespace_id": agent._world.namespace_id.hex(),
|
|
332
|
+
"sealed_count": len(agent._world.sealed),
|
|
333
|
+
"dispute_count": len(agent._world.disputes),
|
|
334
|
+
"tree_size": agent._world.ts.size() if agent._world.ts else 0,
|
|
335
|
+
}
|
|
336
|
+
if agent._world.storage is not None:
|
|
337
|
+
try:
|
|
338
|
+
payload["storage"] = agent._world.storage.stats()
|
|
339
|
+
except Exception:
|
|
340
|
+
payload["storage"] = {"error": "stats unavailable"}
|
|
341
|
+
await _send_json(send, status=200, payload=payload)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def _handle_calibration(
|
|
345
|
+
agent: HyperMindAgent,
|
|
346
|
+
body: bytes,
|
|
347
|
+
send: _Send,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""POST /v1/calibration — return per-(agent_kid, topic) Brier history."""
|
|
350
|
+
try:
|
|
351
|
+
payload = json.loads(body) if body else {}
|
|
352
|
+
except json.JSONDecodeError as exc:
|
|
353
|
+
await _err(send, 400, f"invalid JSON: {exc}")
|
|
354
|
+
return
|
|
355
|
+
kid_hex = payload.get("agent_kid")
|
|
356
|
+
topic = payload.get("topic", "default")
|
|
357
|
+
if not kid_hex:
|
|
358
|
+
await _err(send, 400, "missing field: agent_kid")
|
|
359
|
+
return
|
|
360
|
+
try:
|
|
361
|
+
kid = _parse_kid(kid_hex)
|
|
362
|
+
except ValueError:
|
|
363
|
+
await _err(send, 400, f"invalid kid hex: {kid_hex!r}")
|
|
364
|
+
return
|
|
365
|
+
if agent._world is None:
|
|
366
|
+
await _err(send, 404, "no world attached")
|
|
367
|
+
return
|
|
368
|
+
out = agent._world.reputation.brier_tracker.to_dict(kid, topic)
|
|
369
|
+
if out["n_observations"] == 0:
|
|
370
|
+
await _err(send, 404, f"no calibration history for {kid_hex[:16]}…/{topic}")
|
|
371
|
+
return
|
|
372
|
+
await _send_json(send, status=200, payload=out)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _handle_cost(
|
|
376
|
+
agent: HyperMindAgent,
|
|
377
|
+
query_params: dict[str, str],
|
|
378
|
+
send: _Send,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""GET /v1/cost — return TokenCostTracker aggregate."""
|
|
381
|
+
if agent._world is None or agent._world.cost_tracker is None:
|
|
382
|
+
await _send_json(
|
|
383
|
+
send,
|
|
384
|
+
status=200,
|
|
385
|
+
payload={
|
|
386
|
+
"n_calls": 0,
|
|
387
|
+
"total_tokens": 0,
|
|
388
|
+
"total_usd": 0.0,
|
|
389
|
+
"by_model": {},
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
tracker = agent._world.cost_tracker
|
|
394
|
+
if "agent_kid" in query_params or "model" in query_params:
|
|
395
|
+
kid_hex = query_params.get("agent_kid")
|
|
396
|
+
model = query_params.get("model")
|
|
397
|
+
kid_bytes = None
|
|
398
|
+
if kid_hex:
|
|
399
|
+
try:
|
|
400
|
+
kid_bytes = _parse_kid(kid_hex)
|
|
401
|
+
except ValueError:
|
|
402
|
+
await _err(send, 400, f"invalid kid hex: {kid_hex!r}")
|
|
403
|
+
return
|
|
404
|
+
await _send_json(
|
|
405
|
+
send,
|
|
406
|
+
status=200,
|
|
407
|
+
payload={
|
|
408
|
+
"filter": {"agent_kid": kid_hex, "model": model},
|
|
409
|
+
"total_usd": tracker.total_usd(agent_kid=kid_bytes, model=model),
|
|
410
|
+
"total_tokens": tracker.total_tokens(agent_kid=kid_bytes, model=model),
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
return
|
|
414
|
+
await _send_json(send, status=200, payload=tracker.to_dict())
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def _handle_eval_compare(
|
|
418
|
+
agent: HyperMindAgent,
|
|
419
|
+
body: bytes,
|
|
420
|
+
send: _Send,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""POST /v1/eval/compare — A/B compare static-config responders.
|
|
423
|
+
|
|
424
|
+
For safety the HTTP endpoint accepts only StaticResponder configs;
|
|
425
|
+
callers wanting LLM comparison should use the Python API.
|
|
426
|
+
"""
|
|
427
|
+
from hypermind.eval import compare_responders
|
|
428
|
+
from hypermind.responders import StaticResponder
|
|
429
|
+
from hypermind.uncertainty import Uncertainty
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
payload = json.loads(body) if body else {}
|
|
433
|
+
except json.JSONDecodeError as exc:
|
|
434
|
+
await _err(send, 400, f"invalid JSON: {exc}")
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
questions = payload.get("question_set") or payload.get("questions") or []
|
|
438
|
+
responders_cfg = payload.get("responders") or {}
|
|
439
|
+
if not questions or not responders_cfg:
|
|
440
|
+
await _err(send, 400, "need both 'question_set' and 'responders'")
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
built: dict[str, Any] = {}
|
|
444
|
+
for name, cfg in responders_cfg.items():
|
|
445
|
+
kind = cfg.get("kind", "static")
|
|
446
|
+
if kind != "static":
|
|
447
|
+
await _err(send, 400, f"only kind=static supported in HTTP API; got {kind!r}")
|
|
448
|
+
return
|
|
449
|
+
confidence = float(cfg.get("confidence", 0.5))
|
|
450
|
+
unc = Uncertainty.beta(
|
|
451
|
+
max(1.0, confidence * 10),
|
|
452
|
+
max(1.0, (1 - confidence) * 10),
|
|
453
|
+
)
|
|
454
|
+
built[name] = StaticResponder(unc)
|
|
455
|
+
|
|
456
|
+
report = await compare_responders(
|
|
457
|
+
questions,
|
|
458
|
+
built,
|
|
459
|
+
question_set_id=payload.get("question_set_id", ""),
|
|
460
|
+
)
|
|
461
|
+
await _send_json(send, status=200, payload=report.to_dict())
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
async def _handle_events_stream(
|
|
465
|
+
agent: HyperMindAgent,
|
|
466
|
+
send: _Send,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""GET /v1/events/stream — Server-Sent Events of recorder events.
|
|
469
|
+
|
|
470
|
+
Each event is one ``LogRecord`` serialised as JSON with these fields:
|
|
471
|
+
ts_wall_ms, namespace, agent_id, event_type, severity, fields
|
|
472
|
+
|
|
473
|
+
The endpoint never closes — clients (e.g. ``EventSource`` in a
|
|
474
|
+
browser) keep the connection open and reconnect automatically on
|
|
475
|
+
drop. We send a heartbeat comment every 15s to keep proxies happy.
|
|
476
|
+
"""
|
|
477
|
+
if agent._world is None:
|
|
478
|
+
await _err(send, 404, "no world attached")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
recorder = agent._world.recorder
|
|
482
|
+
# SSE headers
|
|
483
|
+
await send(
|
|
484
|
+
{
|
|
485
|
+
"type": "http.response.start",
|
|
486
|
+
"status": 200,
|
|
487
|
+
"headers": [
|
|
488
|
+
(b"content-type", b"text/event-stream"),
|
|
489
|
+
(b"cache-control", b"no-cache"),
|
|
490
|
+
(b"connection", b"keep-alive"),
|
|
491
|
+
(b"access-control-allow-origin", b"*"),
|
|
492
|
+
],
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Send an initial comment so the client knows we're connected
|
|
497
|
+
await send(
|
|
498
|
+
{
|
|
499
|
+
"type": "http.response.body",
|
|
500
|
+
"body": b": connected\n\n",
|
|
501
|
+
"more_body": True,
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
async for record in recorder.tail():
|
|
507
|
+
payload = {
|
|
508
|
+
"ts_wall_ms": record.ts_wall_ms,
|
|
509
|
+
"namespace": record.namespace,
|
|
510
|
+
"agent_id": record.agent_id,
|
|
511
|
+
"event_type": record.event_type,
|
|
512
|
+
"severity": record.severity,
|
|
513
|
+
"fields": record.fields,
|
|
514
|
+
}
|
|
515
|
+
chunk = f"data: {json.dumps(payload)}\n\n".encode()
|
|
516
|
+
await send(
|
|
517
|
+
{
|
|
518
|
+
"type": "http.response.body",
|
|
519
|
+
"body": chunk,
|
|
520
|
+
"more_body": True,
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
except (asyncio.CancelledError, GeneratorExit):
|
|
524
|
+
# Client disconnected — clean exit
|
|
525
|
+
return
|
|
526
|
+
except Exception:
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
async def _handle_replay(
|
|
531
|
+
agent: HyperMindAgent,
|
|
532
|
+
body: bytes,
|
|
533
|
+
send: _Send,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""POST /v1/replay — replay an audit log (provided as JSONL bytes)."""
|
|
536
|
+
from hypermind.eval.replay import replay_lines
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
payload = json.loads(body) if body else {}
|
|
540
|
+
except json.JSONDecodeError as exc:
|
|
541
|
+
await _err(send, 400, f"invalid JSON: {exc}")
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
audit_text = payload.get("audit_text", "")
|
|
545
|
+
if not audit_text:
|
|
546
|
+
await _err(send, 400, "missing 'audit_text' (JSONL string)")
|
|
547
|
+
return
|
|
548
|
+
verify_signatures = bool(payload.get("verify_signatures", True))
|
|
549
|
+
result = replay_lines(
|
|
550
|
+
audit_text.splitlines(),
|
|
551
|
+
verify_signatures=verify_signatures,
|
|
552
|
+
)
|
|
553
|
+
await _send_json(send, status=200, payload=result.to_dict())
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# --------------------------------------------------------------------------
|
|
557
|
+
# Router
|
|
558
|
+
# --------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def management_app(
|
|
562
|
+
agent: HyperMindAgent,
|
|
563
|
+
) -> Callable[[_Scope, _Receive, _Send], Awaitable[None]]:
|
|
564
|
+
"""Return an ASGI 3 app that exposes ``agent`` over HTTP.
|
|
565
|
+
|
|
566
|
+
Routes:
|
|
567
|
+
GET /healthz — liveness probe
|
|
568
|
+
GET /metrics — Prometheus scrape (text 0.0.4)
|
|
569
|
+
GET /v1/stats — agent + world summary
|
|
570
|
+
GET /v1/introspect — full introspection payload
|
|
571
|
+
POST /v1/claims — publish a claim
|
|
572
|
+
GET /v1/claims/{kid} — fetch a claim by kid
|
|
573
|
+
GET /v1/audit?since_hlc=... — stream signed audit log
|
|
574
|
+
GET /v1/reputation/{kid}?topic=... — read reputation
|
|
575
|
+
GET /v1/disputes — list active disputes
|
|
576
|
+
POST /v1/consult — run a panel consult
|
|
577
|
+
"""
|
|
578
|
+
obs_app = _obs_app(recorder=agent._world.recorder if agent._world else None)
|
|
579
|
+
|
|
580
|
+
async def app(scope: _Scope, receive: _Receive, send: _Send) -> None:
|
|
581
|
+
if scope["type"] == "lifespan":
|
|
582
|
+
while True:
|
|
583
|
+
msg = await receive()
|
|
584
|
+
if msg["type"] == "lifespan.startup":
|
|
585
|
+
await send({"type": "lifespan.startup.complete"})
|
|
586
|
+
elif msg["type"] == "lifespan.shutdown":
|
|
587
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
if scope["type"] != "http":
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
method: str = scope.get("method", "GET").upper()
|
|
594
|
+
path: str = scope.get("path", "/")
|
|
595
|
+
qs: bytes = scope.get("query_string", b"")
|
|
596
|
+
params = _parse_qs(qs)
|
|
597
|
+
|
|
598
|
+
# Delegate /healthz, /metrics to the observability app
|
|
599
|
+
if path in ("/healthz", "/metrics"):
|
|
600
|
+
await obs_app(scope, receive, send)
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
# ---- v1 routes ---------------------------------------------------
|
|
604
|
+
if method == "GET" and path == "/v1/stats":
|
|
605
|
+
await _handle_stats(agent, send)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
if method == "GET" and path == "/v1/introspect":
|
|
609
|
+
await _handle_introspect(agent, send)
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
if method == "POST" and path == "/v1/claims":
|
|
613
|
+
body = await _read_body(receive)
|
|
614
|
+
await _handle_publish(agent, body, send)
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
if method == "GET" and path.startswith("/v1/claims/"):
|
|
618
|
+
kid_hex = path[len("/v1/claims/") :]
|
|
619
|
+
await _handle_get_claim(agent, kid_hex, send)
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
if method == "GET" and path == "/v1/audit":
|
|
623
|
+
since = params.get("since_hlc")
|
|
624
|
+
await _handle_audit(agent, since, send)
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
if method == "GET" and path.startswith("/v1/reputation/"):
|
|
628
|
+
kid_hex = path[len("/v1/reputation/") :]
|
|
629
|
+
topic = params.get("topic")
|
|
630
|
+
await _handle_reputation(agent, kid_hex, topic, send)
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
if method == "GET" and path == "/v1/disputes":
|
|
634
|
+
await _handle_disputes(agent, send)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
if method == "POST" and path == "/v1/consult":
|
|
638
|
+
body = await _read_body(receive)
|
|
639
|
+
await _handle_consult(agent, body, send)
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
# ---- v0.6 eval routes ----
|
|
643
|
+
if method == "POST" and path == "/v1/calibration":
|
|
644
|
+
body = await _read_body(receive)
|
|
645
|
+
await _handle_calibration(agent, body, send)
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
if method == "GET" and path == "/v1/cost":
|
|
649
|
+
await _handle_cost(agent, params, send)
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
if method == "POST" and path == "/v1/eval/compare":
|
|
653
|
+
body = await _read_body(receive)
|
|
654
|
+
await _handle_eval_compare(agent, body, send)
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
if method == "POST" and path == "/v1/replay":
|
|
658
|
+
body = await _read_body(receive)
|
|
659
|
+
await _handle_replay(agent, body, send)
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
if method == "GET" and path == "/v1/events/stream":
|
|
663
|
+
await _handle_events_stream(agent, send)
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# Fallback
|
|
667
|
+
await _err(send, 404, f"unknown route: {method} {path}")
|
|
668
|
+
|
|
669
|
+
return app
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _parse_qs(qs: bytes) -> dict[str, str]:
|
|
673
|
+
"""Tiny QS parser — first value wins, no list-of-values support."""
|
|
674
|
+
out: dict[str, str] = {}
|
|
675
|
+
for pair in qs.split(b"&"):
|
|
676
|
+
if not pair:
|
|
677
|
+
continue
|
|
678
|
+
if b"=" in pair:
|
|
679
|
+
k, v = pair.split(b"=", 1)
|
|
680
|
+
else:
|
|
681
|
+
k, v = pair, b""
|
|
682
|
+
try:
|
|
683
|
+
out[k.decode("utf-8")] = v.decode("utf-8")
|
|
684
|
+
except UnicodeDecodeError:
|
|
685
|
+
continue
|
|
686
|
+
return out
|