hypermind 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
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