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
@@ -0,0 +1,260 @@
1
+ """Mind collaboration records — intent, trace, dissent.
2
+
3
+ These are *application-layer* records carried inside the existing
4
+ ``SEAL`` SignedStatement payload. We deliberately avoid extending the
5
+ COSE/CBOR wire format (which would require an envelope_schema_hash bump,
6
+ v0.2 wire-version negotiation, reference_verifier sync, and updated
7
+ Appendix Z conformance vectors).
8
+
9
+ Carrying mind-records as typed payloads keeps:
10
+ - A-ALG-BOUND intact (record_type stays "SEAL", every discriminator
11
+ in the protected header)
12
+ - reference_verifier interop intact
13
+ - transparency-log auditability intact (each is sealed and cosigned
14
+ via TransparencyService.append_and_cosign)
15
+ - reputation accountability intact (signed by issuer, citeable like
16
+ any other claim)
17
+
18
+ A future v1 wire-version may promote these to first-class record_types;
19
+ the on-disk codec defined here will be the canonical encoding.
20
+
21
+ Schema (CBOR map, deterministic):
22
+ { 0: <kind str>, 1: <topic str>, 2: <body map>, 3: <hlc_ms int>,
23
+ 4: <issuer_kid bytes>, 5: <version int> }
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ from dataclasses import dataclass, field
30
+ from typing import Any, Literal
31
+
32
+ import cbor2
33
+
34
+
35
+ def make_target_kid_commitment(target_kid: bytes) -> bytes:
36
+ """P3-02: Compute the SHA-256 commitment over a target kid.
37
+
38
+ Use this to populate ``IntentRecord.target_kid_commitment`` when
39
+ you want to announce a dispute intent without revealing the full
40
+ target kid on the gossip bus.
41
+ """
42
+ return hashlib.sha256(target_kid).digest()
43
+
44
+
45
+ MIND_RECORD_VERSION = 1
46
+ MindRecordKind = Literal["intent", "trace", "dissent"]
47
+
48
+ # DoS gate: oversize CBOR payloads are rejected before parsing. Mind records
49
+ # are small typed envelopes; 64 KiB is comfortable headroom over the largest
50
+ # realistic intent/trace/dissent body.
51
+ MAX_MIND_RECORD_BYTES = 64 * 1024
52
+
53
+ _KEY_KIND = 0
54
+ _KEY_TOPIC = 1
55
+ _KEY_BODY = 2
56
+ _KEY_HLC_MS = 3
57
+ _KEY_ISSUER_KID = 4
58
+ _KEY_VERSION = 5
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class IntentRecord:
63
+ """Agent announces an upcoming action so peers can pre-stage / dedupe."""
64
+
65
+ topic: str
66
+ action: str # one of "consult", "publish", "dispute", ...
67
+ target_kid: bytes | None = None # for dispute intents
68
+ expected_completion_ms: int | None = None
69
+ note: str = ""
70
+ # P3-02: privacy-preserving commitment to the target kid.
71
+ # When set, this is SHA-256(target_kid) so peers can match dispute
72
+ # intents against claims they know without exposing the full kid to
73
+ # agents that don't need it. If both target_kid and
74
+ # target_kid_commitment are set, target_kid takes precedence for
75
+ # local use; target_kid_commitment is what is broadcast on the wire.
76
+ target_kid_commitment: bytes | None = None
77
+
78
+ kind: str = field(default="intent", init=False)
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class TraceRecord:
83
+ """Reasoning trace for a published claim — the citations that shaped it.
84
+
85
+ First-class on-wire: makes the citation graph queryable from any
86
+ transparency-log audit. Closes failure-modes.md:49 (citation-graph
87
+ attack — citations were previously local-store only).
88
+ """
89
+
90
+ topic: str
91
+ claim_kid: bytes # the claim this trace explains
92
+ parent_kids: tuple[bytes, ...]
93
+ consult_kids: tuple[bytes, ...] = ()
94
+ note: str = ""
95
+
96
+ kind: str = field(default="trace", init=False)
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class DissentRecord:
101
+ """Lightweight pre-dispute signal: 'I see this differently'.
102
+
103
+ No bond posted, no FSM transition. Surfaces minority disagreement
104
+ before it escalates to a full bonded dispute — closes the herding
105
+ failure mode (failure-modes.md:17) by making divergence visible
106
+ early. Carries an optional reasoning note so peers can adjudicate
107
+ whether to weight it.
108
+ """
109
+
110
+ topic: str
111
+ target_kid: bytes
112
+ own_posterior: float
113
+ note: str = ""
114
+ margin: float = 0.0 # |own_posterior - peer_posterior|
115
+
116
+ kind: str = field(default="dissent", init=False)
117
+
118
+
119
+ # -----------------------------------------------------------------------
120
+ # Encoding
121
+ # -----------------------------------------------------------------------
122
+
123
+
124
+ def encode_mind_record(
125
+ record: IntentRecord | TraceRecord | DissentRecord,
126
+ *,
127
+ issuer_kid: bytes,
128
+ hlc_ms: int,
129
+ ) -> bytes:
130
+ """Serialise a mind record as deterministic CBOR for SEAL payload."""
131
+ if isinstance(record, IntentRecord):
132
+ body: dict[str, Any] = {
133
+ "action": record.action,
134
+ "note": record.note,
135
+ }
136
+ if record.target_kid is not None:
137
+ body["target_kid"] = record.target_kid
138
+ if record.expected_completion_ms is not None:
139
+ body["expected_completion_ms"] = record.expected_completion_ms
140
+ # P3-02: include the commitment if present
141
+ if record.target_kid_commitment is not None:
142
+ body["target_kid_commitment"] = record.target_kid_commitment
143
+ elif isinstance(record, TraceRecord):
144
+ body = {
145
+ "claim_kid": record.claim_kid,
146
+ "parent_kids": list(record.parent_kids),
147
+ "consult_kids": list(record.consult_kids),
148
+ "note": record.note,
149
+ }
150
+ elif isinstance(record, DissentRecord):
151
+ body = {
152
+ "target_kid": record.target_kid,
153
+ "own_posterior": record.own_posterior,
154
+ "margin": record.margin,
155
+ "note": record.note,
156
+ }
157
+ else: # pragma: no cover - typeguard
158
+ raise TypeError(f"unknown mind record type: {type(record).__name__}")
159
+
160
+ envelope = {
161
+ _KEY_KIND: record.kind,
162
+ _KEY_TOPIC: record.topic,
163
+ _KEY_BODY: body,
164
+ _KEY_HLC_MS: int(hlc_ms),
165
+ _KEY_ISSUER_KID: bytes(issuer_kid),
166
+ _KEY_VERSION: MIND_RECORD_VERSION,
167
+ }
168
+ return cbor2.dumps(envelope, canonical=True)
169
+
170
+
171
+ def decode_mind_record(
172
+ raw: bytes,
173
+ *,
174
+ expected_signer_kid: bytes,
175
+ ) -> tuple[IntentRecord | TraceRecord | DissentRecord, bytes, int]:
176
+ """Decode a mind-record payload. Returns (record, issuer_kid, hlc_ms).
177
+
178
+ The ``expected_signer_kid`` is the kid of the outer SignedStatement
179
+ signer; it MUST match the embedded ``issuer_kid`` to prevent a kid
180
+ spoofing attack where a valid signer wraps a payload that claims a
181
+ different agent as issuer.
182
+
183
+ Raises ``ValueError`` on malformed/unsupported records, oversize
184
+ payloads, or issuer_kid mismatch.
185
+ """
186
+ if len(raw) > MAX_MIND_RECORD_BYTES:
187
+ raise ValueError(f"mind record too large: {len(raw)} > {MAX_MIND_RECORD_BYTES}")
188
+ try:
189
+ env = cbor2.loads(raw)
190
+ except Exception as e:
191
+ raise ValueError(f"mind record decode: not CBOR: {e}") from e
192
+ if not isinstance(env, dict):
193
+ raise ValueError("mind record decode: not a map")
194
+ version = env.get(_KEY_VERSION)
195
+ if version != MIND_RECORD_VERSION:
196
+ raise ValueError(f"mind record decode: unsupported version {version!r}")
197
+ kind = env.get(_KEY_KIND)
198
+ topic = env.get(_KEY_TOPIC)
199
+ body = env.get(_KEY_BODY) or {}
200
+ hlc_ms = int(env.get(_KEY_HLC_MS, 0))
201
+ issuer_kid = env.get(_KEY_ISSUER_KID, b"")
202
+ if not isinstance(topic, str) or not isinstance(body, dict):
203
+ raise ValueError("mind record decode: malformed topic/body")
204
+ if not isinstance(issuer_kid, (bytes, bytearray)):
205
+ raise ValueError("mind record decode: issuer_kid must be bytes")
206
+ issuer_kid_bytes = bytes(issuer_kid)
207
+ if issuer_kid_bytes != bytes(expected_signer_kid):
208
+ raise ValueError(
209
+ "issuer_kid mismatch: payload claims "
210
+ f"{issuer_kid_bytes[:8].hex()}, signer is "
211
+ f"{bytes(expected_signer_kid)[:8].hex()}"
212
+ )
213
+
214
+ if kind == "intent":
215
+ raw_commitment = body.get("target_kid_commitment")
216
+ rec: Any = IntentRecord(
217
+ topic=topic,
218
+ action=str(body.get("action", "")),
219
+ target_kid=body.get("target_kid"),
220
+ expected_completion_ms=body.get("expected_completion_ms"),
221
+ note=str(body.get("note", "")),
222
+ # P3-02: decode target_kid_commitment if present
223
+ target_kid_commitment=(
224
+ bytes(raw_commitment) if isinstance(raw_commitment, (bytes, bytearray)) else None
225
+ ),
226
+ )
227
+ elif kind == "trace":
228
+ rec = TraceRecord(
229
+ topic=topic,
230
+ claim_kid=bytes(body.get("claim_kid", b"")),
231
+ parent_kids=tuple(bytes(p) for p in body.get("parent_kids", ())),
232
+ consult_kids=tuple(bytes(c) for c in body.get("consult_kids", ())),
233
+ note=str(body.get("note", "")),
234
+ )
235
+ elif kind == "dissent":
236
+ rec = DissentRecord(
237
+ topic=topic,
238
+ target_kid=bytes(body.get("target_kid", b"")),
239
+ own_posterior=float(body.get("own_posterior", 0.0)),
240
+ margin=float(body.get("margin", 0.0)),
241
+ note=str(body.get("note", "")),
242
+ )
243
+ else:
244
+ raise ValueError(f"mind record decode: unknown kind {kind!r}")
245
+ return rec, issuer_kid_bytes, hlc_ms
246
+
247
+
248
+ def is_mind_record(raw: bytes) -> bool:
249
+ """Cheap discriminator — does this look like a mind record envelope?"""
250
+ if len(raw) > MAX_MIND_RECORD_BYTES:
251
+ return False
252
+ try:
253
+ env = cbor2.loads(raw)
254
+ except Exception:
255
+ return False
256
+ return (
257
+ isinstance(env, dict)
258
+ and env.get(_KEY_VERSION) == MIND_RECORD_VERSION
259
+ and env.get(_KEY_KIND) in ("intent", "trace", "dissent")
260
+ )
@@ -0,0 +1,190 @@
1
+ """Role specialisation — Scout, Sceptic, Generalist, Synthesist, Mediator, Oracle.
2
+
3
+ A Role is a declarative bundle of (policies, caveats) that composes
4
+ with the existing CapToken chain. Picking a role for an agent is the
5
+ fastest path to a swarm of differentiated specialists rather than a
6
+ swarm of clones.
7
+
8
+ Spec: docs/spec/20-swarm-intelligence.md §20.2.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from enum import StrEnum
15
+
16
+ from hypermind.capability import Caveat
17
+ from hypermind.mind.policy import (
18
+ ActionPolicy,
19
+ CheckpointOnSignalGain,
20
+ ConsultOnDisagreement,
21
+ DisputeOnContradiction,
22
+ PublishWhenConfident,
23
+ )
24
+
25
+
26
+ class RoleKind(StrEnum):
27
+ SCOUT = "scout"
28
+ SCEPTIC = "sceptic"
29
+ GENERALIST = "generalist"
30
+ SYNTHESIST = "synthesist"
31
+ MEDIATOR = "mediator"
32
+ ORACLE = "oracle"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Role:
37
+ """A role bundle — policies + caveats + descriptive metadata."""
38
+
39
+ kind: RoleKind
40
+ policies: tuple[ActionPolicy, ...]
41
+ caveats: tuple[Caveat, ...] = ()
42
+ description: str = ""
43
+ publishes_primary: bool = True # False for Mediator/Oracle (they don't publish raw claims)
44
+
45
+ def policy_list(self) -> list[ActionPolicy]:
46
+ return list(self.policies)
47
+
48
+ def caveat_list(self) -> list[Caveat]:
49
+ return list(self.caveats)
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Built-in roles
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ def scout(*, topic: str = "*") -> Role:
58
+ """Low-confidence threshold, high consult frequency, broad caveat."""
59
+ return Role(
60
+ kind=RoleKind.SCOUT,
61
+ policies=(
62
+ PublishWhenConfident(threshold=0.65, priority=40),
63
+ ConsultOnDisagreement(divergence=0.15, priority=55),
64
+ CheckpointOnSignalGain(kl_threshold=0.05, priority=30),
65
+ ),
66
+ caveats=(Caveat.topic(topic), Caveat.rate("60/min")),
67
+ description="early-warning, broad coverage, frequent consults",
68
+ )
69
+
70
+
71
+ def sceptic(*, topic: str = "*") -> Role:
72
+ """Aggressive disputes, high publish bar."""
73
+ return Role(
74
+ kind=RoleKind.SCEPTIC,
75
+ policies=(
76
+ PublishWhenConfident(threshold=0.92, priority=30),
77
+ DisputeOnContradiction(min_gap=0.3, min_rep_delta=0.10, priority=70),
78
+ ConsultOnDisagreement(divergence=0.25, priority=40),
79
+ ),
80
+ caveats=(Caveat.topic(topic), Caveat.rate("30/min")),
81
+ description="contrarian; raises the bar on consensus",
82
+ )
83
+
84
+
85
+ def generalist(*, topic: str = "*") -> Role:
86
+ """Balanced default."""
87
+ return Role(
88
+ kind=RoleKind.GENERALIST,
89
+ policies=(
90
+ PublishWhenConfident(threshold=0.8, priority=50),
91
+ ConsultOnDisagreement(divergence=0.3, priority=40),
92
+ DisputeOnContradiction(min_gap=0.4, min_rep_delta=0.15, priority=60),
93
+ CheckpointOnSignalGain(kl_threshold=0.1, priority=30),
94
+ ),
95
+ caveats=(Caveat.topic(topic),),
96
+ description="balanced default; the v0.10 mind layer's behaviour",
97
+ )
98
+
99
+
100
+ def synthesist(*, topic: str = "*") -> Role:
101
+ """Aggregator only — caveat enforces synthesis claim type."""
102
+ return Role(
103
+ kind=RoleKind.SYNTHESIST,
104
+ policies=(
105
+ PublishWhenConfident(threshold=0.85, priority=40),
106
+ ConsultOnDisagreement(divergence=0.2, priority=55),
107
+ ),
108
+ caveats=(
109
+ Caveat.topic(topic),
110
+ Caveat.claim_type("synthesis"),
111
+ Caveat.rate("10/min"),
112
+ ),
113
+ description="only publishes synthesis claims; ≥3 parents required (enforced by application)",
114
+ )
115
+
116
+
117
+ def mediator(*, topic: str = "*") -> Role:
118
+ """Adjudication role; cannot publish primary claims (caveat-enforced)."""
119
+ return Role(
120
+ kind=RoleKind.MEDIATOR,
121
+ policies=(DisputeOnContradiction(min_gap=0.5, min_rep_delta=0.2, priority=80),),
122
+ caveats=(
123
+ Caveat.topic(topic),
124
+ Caveat.claim_type("synthesis"), # mediators may only publish summaries
125
+ ),
126
+ description="VRF-selected adjudicator; uses select_mediator() for picks",
127
+ publishes_primary=False,
128
+ )
129
+
130
+
131
+ def oracle(*, topic: str = "*") -> Role:
132
+ """Ground-truth resolver only — publishes oracle_resolution claims."""
133
+ return Role(
134
+ kind=RoleKind.ORACLE,
135
+ policies=(), # oracle doesn't deliberate; it resolves
136
+ caveats=(
137
+ Caveat.topic(topic),
138
+ Caveat.claim_type("oracle_resolution"),
139
+ ),
140
+ description="closes the loop; not a deliberator",
141
+ publishes_primary=False,
142
+ )
143
+
144
+
145
+ _BUILTIN: dict[str, callable] = {
146
+ "scout": scout,
147
+ "sceptic": sceptic,
148
+ "generalist": generalist,
149
+ "synthesist": synthesist,
150
+ "mediator": mediator,
151
+ "oracle": oracle,
152
+ }
153
+
154
+
155
+ def role_by_name(name: str, *, topic: str = "*") -> Role:
156
+ """Look up a built-in role by its lowercase name."""
157
+ if name not in _BUILTIN:
158
+ raise ValueError(f"unknown role {name!r}; valid: {sorted(_BUILTIN)}")
159
+ return _BUILTIN[name](topic=topic)
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Composition — assemble a swarm spec
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ @dataclass
168
+ class SwarmComposition:
169
+ """A swarm spec — counts per role. Used by the harness + examples."""
170
+
171
+ counts: dict[RoleKind, int] = field(default_factory=dict)
172
+
173
+ @classmethod
174
+ def of(cls, **kwargs: int) -> SwarmComposition:
175
+ """``SwarmComposition.of(scout=3, generalist=2, oracle=1)``."""
176
+ out = cls()
177
+ for k, n in kwargs.items():
178
+ out.counts[RoleKind(k)] = int(n)
179
+ return out
180
+
181
+ def total(self) -> int:
182
+ return sum(self.counts.values())
183
+
184
+ def expand(self, *, topic: str = "*") -> list[Role]:
185
+ """Return the flat list of roles per the configured counts."""
186
+ out: list[Role] = []
187
+ for kind, n in self.counts.items():
188
+ for _ in range(int(n)):
189
+ out.append(role_by_name(kind.value, topic=topic))
190
+ return out
@@ -0,0 +1,41 @@
1
+ """SybilGuard — cap influence of new agents in deliberator decisions.
2
+
3
+ Failure mode (design/failure-modes.md:59-78): a vouched newcomer can
4
+ inject up to ~30 claims before the calibration loop detects bad
5
+ predictions. With N pre-built sybils, that scales to N × 30
6
+ undetected injections. The vouch cap (5% of voucher α) is the
7
+ network-side defence; this is the agent-side defence — the
8
+ *deliberator* of an honest agent should refuse to give a newcomer
9
+ full weight in routing/synthesis until they've earned a track record
10
+ of resolved outcomes.
11
+
12
+ API mirrors the existing 5%-of-α vouch cap pattern in
13
+ src/hypermind/knowledge/reputation.py:136-157.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+
20
+
21
+ @dataclass
22
+ class SybilGuard:
23
+ """Cap an agent's effective weight by their observation count.
24
+
25
+ ``min_observations`` is the threshold for full weight. Below that,
26
+ weight is linearly attenuated down to ``floor_weight``.
27
+ """
28
+
29
+ min_observations: int = 30
30
+ floor_weight: float = 0.05
31
+
32
+ def effective_weight(self, raw_weight: float, n_observations: int) -> float:
33
+ if n_observations >= self.min_observations:
34
+ return raw_weight
35
+ ratio = max(0, n_observations) / max(1, self.min_observations)
36
+ capped = self.floor_weight + (1.0 - self.floor_weight) * ratio
37
+ return raw_weight * capped
38
+
39
+ def is_advisory_only(self, n_observations: int) -> bool:
40
+ """True when the agent's weight is below 50% of full."""
41
+ return n_observations < (self.min_observations // 2)
@@ -0,0 +1,166 @@
1
+ """WorldModel + LookaheadPolicy — model-based deliberation.
2
+
3
+ Agents that *plan* against an internal predictive model dramatically
4
+ outperform reactive ones (cf. MuZero/Dreamer). We expose a Protocol
5
+ so production deployments can swap in any predictor (LLM-backed,
6
+ Bayesian, learned model). The sandbox impl is a Beta predictor over
7
+ the agent's own (action, outcome) history.
8
+
9
+ Spec: docs/spec/20-swarm-intelligence.md §20.5.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from typing import Protocol, runtime_checkable
17
+
18
+ from hypermind.mind.policy import (
19
+ ActionDecision,
20
+ ActionKind,
21
+ ActionPolicy,
22
+ PolicyContext,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ImaginedOutcome:
28
+ """A predicted future state after a hypothetical action."""
29
+
30
+ posterior: float # P(positive outcome) under this action
31
+ confidence: float # how confident the world model is in the prediction
32
+
33
+
34
+ @runtime_checkable
35
+ class WorldModel(Protocol):
36
+ """Pluggable predictive model. Production may wire an LLM."""
37
+
38
+ def predict(self, action: ActionKind, ctx: PolicyContext) -> ImaginedOutcome: ...
39
+
40
+ def update_with_outcome(
41
+ self, action: ActionKind, ctx: PolicyContext, observed: float
42
+ ) -> None: ...
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Default sandbox impl — Beta predictor
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ @dataclass
51
+ class BetaWorldModel:
52
+ """Per-action Beta posterior over P(good outcome).
53
+
54
+ Each action kind has its own (alpha, beta). The model is a thin
55
+ self-supervised learner: when an outcome is observed, the matching
56
+ bucket updates. Confidence is `1 - 1 / (alpha + beta)` — saturates
57
+ toward 1.0 as data accumulates (more data ⇒ sharper posterior ⇒
58
+ "more confident"). With the Beta(1,1) prior, confidence starts at
59
+ 0.5, matching the prior uncertainty.
60
+ """
61
+
62
+ _state: dict[ActionKind, tuple[float, float]] = field(default_factory=dict)
63
+
64
+ def _get(self, action: ActionKind) -> tuple[float, float]:
65
+ return self._state.get(action, (1.0, 1.0))
66
+
67
+ def predict(self, action: ActionKind, ctx: PolicyContext) -> ImaginedOutcome:
68
+ a, b = self._get(action)
69
+ mean = a / (a + b)
70
+ # Boost predictions when the agent's posterior on the topic
71
+ # already supports this action (e.g. high posterior + publish ⇒ high VOI).
72
+ topic_post = (ctx.working_memory.get("posteriors") or {}).get(ctx.topic)
73
+ if topic_post is not None and action is ActionKind.PUBLISH:
74
+ mean = 0.5 * mean + 0.5 * topic_post
75
+ confidence = 1.0 - 1.0 / (a + b)
76
+ return ImaginedOutcome(posterior=mean, confidence=confidence)
77
+
78
+ def update_with_outcome(self, action: ActionKind, ctx: PolicyContext, observed: float) -> None:
79
+ a, b = self._get(action)
80
+ # observed ∈ [0, 1]; treat as fractional outcome.
81
+ a += float(observed)
82
+ b += 1.0 - float(observed)
83
+ self._state[action] = (a, b)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # LookaheadPolicy — N-step planning with a per-tick budget
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ @dataclass
92
+ class LookaheadPolicy(ActionPolicy):
93
+ """Plan ``depth`` steps with the world_model; pick the highest-EV action.
94
+
95
+ For each candidate action (publish / consult / dispute / checkpoint),
96
+ estimate the expected value-of-information (EVI) under the model.
97
+ Highest EVI wins; ties broken by priority.
98
+
99
+ Per-tick budget is a soft time cap; the policy returns the best
100
+ decision found so far if the budget is exhausted.
101
+ """
102
+
103
+ world_model: WorldModel = field(default_factory=BetaWorldModel)
104
+ depth: int = 2
105
+ budget_ms: float = 10.0
106
+ priority: int = 65
107
+ candidates: tuple[ActionKind, ...] = (
108
+ ActionKind.PUBLISH,
109
+ ActionKind.CONSULT,
110
+ ActionKind.CHECKPOINT,
111
+ )
112
+
113
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
114
+ deadline = time.perf_counter() + (self.budget_ms / 1000.0)
115
+ best: tuple[float, ActionKind] | None = None
116
+ for action in self.candidates:
117
+ if time.perf_counter() >= deadline:
118
+ break
119
+ evi = self._evi(action, ctx, depth=self.depth, deadline=deadline)
120
+ if best is None or evi > best[0]:
121
+ best = (evi, action)
122
+ if best is None or best[0] <= 0.0:
123
+ return None
124
+ evi, action = best
125
+ return ActionDecision(
126
+ kind=action,
127
+ rule_id="MIND-LOOKAHEAD",
128
+ priority=self.priority,
129
+ payload={
130
+ "evi": evi,
131
+ "depth": self.depth,
132
+ "posterior": (ctx.working_memory.get("posteriors") or {}).get(ctx.topic, 0.5),
133
+ },
134
+ reason=f"lookahead depth={self.depth}, EVI={evi:.3f}",
135
+ )
136
+
137
+ def _evi(self, action: ActionKind, ctx: PolicyContext, *, depth: int, deadline: float) -> float:
138
+ """Recursive expected value-of-information."""
139
+ if time.perf_counter() >= deadline:
140
+ return 0.0
141
+ outcome = self.world_model.predict(action, ctx)
142
+ if depth <= 0:
143
+ return outcome.posterior * outcome.confidence
144
+ # Discount future-step EVI by 0.7 — bog-standard horizon discount.
145
+ gamma = 0.7
146
+ next_steps = max(
147
+ (
148
+ self._evi(a, ctx, depth=depth - 1, deadline=deadline)
149
+ for a in self.candidates
150
+ if a is not action # don't loop
151
+ ),
152
+ default=0.0,
153
+ )
154
+ return outcome.posterior * outcome.confidence + gamma * next_steps
155
+
156
+
157
+ def update_world_model_from_resolution(
158
+ world_model: WorldModel,
159
+ *,
160
+ action: ActionKind,
161
+ ctx: PolicyContext,
162
+ oracle_outcome: float,
163
+ ) -> None:
164
+ """Wire helper — called when an oracle resolves a claim that came
165
+ from a deliberator-dispatched action."""
166
+ world_model.update_with_outcome(action, ctx, oracle_outcome)