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/errors.py ADDED
@@ -0,0 +1,218 @@
1
+ """Exception hierarchy + structured `BlockReason`.
2
+
3
+ Each exception carries a `telemetry` dict matching the spec/13-observability.md
4
+ log schema, so errors emitted from the SDK boundary are immediately
5
+ operationally legible without re-wrapping.
6
+
7
+ `BlockReason` (this module) is the structured-refusal contract surfaced by
8
+ `HyperMindAgent.why_blocked(...)` and (eventually, post-WS-F) attached to every
9
+ `HyperMindError` via the `reason=` keyword argument. The `[rule_id] spec_ref:
10
+ hint` `__str__` format is a stability contract: log-parsing regexes
11
+ `r"\\[([A-Z0-9-]+)\\]"` MUST keep working across minor versions.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping
17
+ from dataclasses import dataclass, field
18
+ from datetime import timedelta
19
+ from typing import Any, Literal
20
+
21
+ # -----------------------------------------------------------------------
22
+ # BlockReason — structured refusal carrying rule_id + spec_ref
23
+ # -----------------------------------------------------------------------
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class BlockReason:
28
+ """Structured refusal reason — every Blocked exception carries one.
29
+
30
+ Per draft-hypermind-scitt-contested-claims-profile §16 (Error Taxonomy).
31
+ The bracketed ``[rule_id]`` prefix in ``__str__`` is part of the stable
32
+ contract; log-parsing regexes ``r"\\[([A-Z0-9-]+)\\]"`` MUST keep working
33
+ across minor versions.
34
+
35
+ The legacy fields (``human_explanation``, ``current_value``,
36
+ ``required_value``, ``retry_after_seconds``) are retained for backward
37
+ compatibility with `HyperMindAgent.why_blocked(...)` consumers shipped in
38
+ v0.1.0a0; new code should use ``hint`` and ``telemetry`` instead.
39
+ """
40
+
41
+ rule_id: str
42
+ spec_ref: str
43
+ docs_url: str | None = None
44
+ hint: str | None = None
45
+ telemetry: Mapping[str, Any] = field(default_factory=dict)
46
+ # Legacy (v0.1.0a0) compatibility fields — retained so existing callers of
47
+ # `why_blocked()` keep working unchanged. New code SHOULD prefer `hint`.
48
+ human_explanation: str | None = None
49
+ current_value: Any | None = None
50
+ required_value: Any | None = None
51
+ retry_after_seconds: float | None = None
52
+
53
+ def __str__(self) -> str:
54
+ # Prefer the new `hint` field; fall back to legacy `human_explanation`
55
+ # so v0.1.0a0 call sites that still populate the old field render a
56
+ # useful message instead of "(no hint)".
57
+ message = self.hint or self.human_explanation or "(no hint)"
58
+ return f"[{self.rule_id}] {self.spec_ref}: {message}"
59
+
60
+
61
+ # -----------------------------------------------------------------------
62
+ # Exception hierarchy
63
+ # -----------------------------------------------------------------------
64
+
65
+
66
+ class HyperMindError(Exception):
67
+ """Base class for all SDK errors.
68
+
69
+ Optionally carries a structured ``BlockReason`` so callers can branch on
70
+ ``e.rule_id`` instead of scraping the message. Either pass the reason as
71
+ the first positional argument (``raise HyperMindError(reason)``) or via
72
+ the ``reason=`` keyword.
73
+ """
74
+
75
+ reason: BlockReason | None = None
76
+ rule_id: str | None = None
77
+
78
+ def __init__(
79
+ self,
80
+ message: str | BlockReason,
81
+ *,
82
+ reason: BlockReason | None = None,
83
+ telemetry: dict[str, Any] | None = None,
84
+ ) -> None:
85
+ if isinstance(message, BlockReason):
86
+ self.reason = message
87
+ self.rule_id = message.rule_id
88
+ super().__init__(str(message))
89
+ else:
90
+ self.reason = reason
91
+ self.rule_id = reason.rule_id if reason is not None else None
92
+ super().__init__(message)
93
+ self.telemetry: dict[str, Any] = telemetry or {}
94
+ self.telemetry.setdefault("event_type", type(self).__name__)
95
+ if self.rule_id is not None:
96
+ self.telemetry.setdefault("rule_id", self.rule_id)
97
+
98
+
99
+ class NetworkError(HyperMindError):
100
+ """libp2p dial failure, gossip drop, partition suspected."""
101
+
102
+
103
+ class AuthError(HyperMindError):
104
+ """Capability-token / caveat-chain failure."""
105
+
106
+ def __init__(
107
+ self,
108
+ message: str,
109
+ *,
110
+ failed_caveat: str | None = None,
111
+ reason: BlockReason | None = None,
112
+ telemetry: dict[str, Any] | None = None,
113
+ ) -> None:
114
+ super().__init__(message, reason=reason, telemetry=telemetry)
115
+ self.failed_caveat: str | None = failed_caveat
116
+ if failed_caveat is not None:
117
+ self.telemetry["failed_caveat"] = failed_caveat
118
+
119
+
120
+ class DisputeError(HyperMindError):
121
+ """Malformed dispute, bond-scope mismatch, or frivolous-window violation."""
122
+
123
+
124
+ class RateLimitError(HyperMindError):
125
+ """Capability rate-caveat exceeded. Raised by ``_require_cap`` when a
126
+ ``rate`` caveat on the agent's ``cap_token`` is exhausted for this action
127
+ (P1-03 / HM-RATE-CAP-001).
128
+ """
129
+
130
+
131
+ class RatePauseError(HyperMindError):
132
+ """Gossip rate-cap hit. Carries `retry_after`."""
133
+
134
+ def __init__(
135
+ self,
136
+ message: str,
137
+ *,
138
+ retry_after: timedelta,
139
+ reason: BlockReason | None = None,
140
+ telemetry: dict[str, Any] | None = None,
141
+ ) -> None:
142
+ super().__init__(message, reason=reason, telemetry=telemetry)
143
+ self.retry_after: timedelta = retry_after
144
+ self.telemetry["retry_after_seconds"] = retry_after.total_seconds()
145
+
146
+
147
+ class RampError(HyperMindError):
148
+ """30-day ramp not yet complete or paused."""
149
+
150
+ def __init__(
151
+ self,
152
+ message: str,
153
+ *,
154
+ state: Literal["pending", "paused", "frozen"],
155
+ reason: BlockReason | None = None,
156
+ telemetry: dict[str, Any] | None = None,
157
+ ) -> None:
158
+ super().__init__(message, reason=reason, telemetry=telemetry)
159
+ self.state = state
160
+ self.telemetry["ramp_state"] = state
161
+
162
+
163
+ class KeyCompromiseError(HyperMindError):
164
+ """Signing key has a `compromise_window_start` declared."""
165
+
166
+ def __init__(
167
+ self,
168
+ message: str,
169
+ *,
170
+ compromise_window_start: str,
171
+ reason: BlockReason | None = None,
172
+ telemetry: dict[str, Any] | None = None,
173
+ ) -> None:
174
+ super().__init__(message, reason=reason, telemetry=telemetry)
175
+ self.compromise_window_start = compromise_window_start
176
+ self.telemetry["compromise_window_start"] = compromise_window_start
177
+
178
+
179
+ class UnresolvedPartitionError(HyperMindError):
180
+ """Dispute hit UNRESOLVED_PARTITION terminal — heal-merge produced contradictory closes."""
181
+
182
+
183
+ class ConformanceError(HyperMindError):
184
+ """Wire-level conformance violation — typically thrown by verifier."""
185
+
186
+
187
+ class ProtocolVersionError(HyperMindError):
188
+ """Statement's `protocol_version` is below the namespace's `min_protocol_version`.
189
+
190
+ Carries `rule_id="HM-PROTO-VER-001"` per ROADMAP §0.5.1–0.5.3
191
+ (versioning bridge milestone). Raised pre-signature-verify so the
192
+ refusal is auditable as a policy decision, not a crypto failure.
193
+ """
194
+
195
+ def __init__(
196
+ self,
197
+ message: str,
198
+ *,
199
+ statement_version: int,
200
+ min_required: int,
201
+ reason: BlockReason | None = None,
202
+ telemetry: dict[str, Any] | None = None,
203
+ ) -> None:
204
+ if reason is None:
205
+ reason = BlockReason(
206
+ rule_id="HM-PROTO-VER-001",
207
+ spec_ref="spec/17-upgrade-v1.md §protocol-version-negotiation",
208
+ hint=message,
209
+ telemetry={
210
+ "statement_version": statement_version,
211
+ "min_required": min_required,
212
+ },
213
+ )
214
+ super().__init__(message, reason=reason, telemetry=telemetry)
215
+ self.statement_version = statement_version
216
+ self.min_required = min_required
217
+ self.telemetry["statement_version"] = statement_version
218
+ self.telemetry["min_required"] = min_required
@@ -0,0 +1,49 @@
1
+ """Evaluation primitives — calibration, comparison, replay.
2
+
3
+ This package provides:
4
+ * :class:`BrierScoreTracker` — per-(agent, topic) Brier score history
5
+ and reliability diagram, fed by ``oracle_resolve()``.
6
+ * :func:`compare_responders` (Phase 5) — A/B model comparison harness.
7
+ * :func:`replay_audit_log` (Phase 5) — re-derive verdicts from a
8
+ signed audit log to verify determinism.
9
+
10
+ The Brier tracker is wired into :class:`ReputationKernel` so every
11
+ oracle resolution automatically records the calibration delta. Operators
12
+ read the data via the management API at ``GET /v1/calibration``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from hypermind.eval.calibration import (
18
+ BrierEntry,
19
+ BrierScoreTracker,
20
+ ReliabilityBin,
21
+ brier_score,
22
+ calibration_adjustment,
23
+ reliability_diagram,
24
+ )
25
+ from hypermind.eval.comparison import (
26
+ ComparisonReport,
27
+ ResponderResult,
28
+ compare_responders,
29
+ )
30
+ from hypermind.eval.replay import (
31
+ ReplayResult,
32
+ replay_audit_log,
33
+ replay_lines,
34
+ )
35
+
36
+ __all__ = [
37
+ "BrierEntry",
38
+ "BrierScoreTracker",
39
+ "ComparisonReport",
40
+ "ReliabilityBin",
41
+ "ReplayResult",
42
+ "ResponderResult",
43
+ "brier_score",
44
+ "calibration_adjustment",
45
+ "compare_responders",
46
+ "reliability_diagram",
47
+ "replay_audit_log",
48
+ "replay_lines",
49
+ ]
@@ -0,0 +1,307 @@
1
+ """Closed-loop calibration tracking.
2
+
3
+ When an oracle resolves a claim, the agent that made the prediction
4
+ should be measured: did its stated 70% confidence match observed
5
+ frequencies of YES outcomes? :class:`BrierScoreTracker` records the
6
+ (predicted, outcome) pair, computes per-(agent, topic) Brier scores,
7
+ and produces reliability diagrams.
8
+
9
+ The tracker also derives a :func:`calibration_adjustment` shrinkage
10
+ multiplier ∈ [0.5, 2.0] that callers pass to
11
+ :func:`hypermind.responders.confidence_to_uncertainty` so that
12
+ well-calibrated agents have *sharper* posteriors and overconfident
13
+ agents have *softer* ones — reputation is no longer just "were you
14
+ right?", it's "was your confidence calibrated?".
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import time
20
+ from collections.abc import Iterable
21
+ from dataclasses import dataclass
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Brier-score primitive
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def brier_score(predicted: float, outcome: float) -> float:
29
+ """Squared error between predicted probability and binary outcome.
30
+
31
+ Lower is better. ``brier_score(0.9, 1.0) == 0.01`` (well-calibrated),
32
+ ``brier_score(0.9, 0.0) == 0.81`` (overconfident).
33
+ """
34
+ p = max(0.0, min(1.0, float(predicted)))
35
+ o = max(0.0, min(1.0, float(outcome)))
36
+ return (p - o) ** 2
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # History entries
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ @dataclass
45
+ class BrierEntry:
46
+ """One (predicted, outcome, Brier) record."""
47
+
48
+ ts_wall_ms: int
49
+ predicted_confidence: float
50
+ outcome: float
51
+ brier_score: float
52
+
53
+ def to_dict(self) -> dict:
54
+ return {
55
+ "ts_wall_ms": self.ts_wall_ms,
56
+ "predicted_confidence": self.predicted_confidence,
57
+ "outcome": self.outcome,
58
+ "brier_score": self.brier_score,
59
+ }
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ReliabilityBin:
64
+ """One bin of a reliability diagram."""
65
+
66
+ lower: float
67
+ upper: float
68
+ mean_confidence: float
69
+ mean_outcome: float
70
+ count: int
71
+
72
+ @property
73
+ def gap(self) -> float:
74
+ """Calibration gap for this bin (signed: positive = overconfident)."""
75
+ return self.mean_confidence - self.mean_outcome
76
+
77
+ def to_dict(self) -> dict:
78
+ return {
79
+ "lower": self.lower,
80
+ "upper": self.upper,
81
+ "mean_confidence": self.mean_confidence,
82
+ "mean_outcome": self.mean_outcome,
83
+ "count": self.count,
84
+ "gap": self.gap,
85
+ }
86
+
87
+
88
+ def reliability_diagram(
89
+ entries: Iterable[BrierEntry],
90
+ *,
91
+ n_bins: int = 10,
92
+ ) -> list[ReliabilityBin]:
93
+ """Build a reliability diagram from BrierEntry history.
94
+
95
+ Returns a list of ``n_bins`` :class:`ReliabilityBin` records. Empty
96
+ bins are omitted from the output. A perfectly-calibrated agent has
97
+ every bin's ``gap`` near zero.
98
+ """
99
+ if n_bins < 1:
100
+ raise ValueError("n_bins must be >= 1")
101
+
102
+ rows = list(entries)
103
+ if not rows:
104
+ return []
105
+
106
+ edges = [i / n_bins for i in range(n_bins + 1)]
107
+ bins: list[list[BrierEntry]] = [[] for _ in range(n_bins)]
108
+ for r in rows:
109
+ # Find which bucket the predicted confidence falls into.
110
+ # Edge case: confidence == 1.0 → last bin.
111
+ c = max(0.0, min(0.9999999, r.predicted_confidence))
112
+ idx = int(c * n_bins)
113
+ bins[idx].append(r)
114
+
115
+ out: list[ReliabilityBin] = []
116
+ for i, b in enumerate(bins):
117
+ if not b:
118
+ continue
119
+ mean_conf = sum(e.predicted_confidence for e in b) / len(b)
120
+ mean_outcome = sum(e.outcome for e in b) / len(b)
121
+ out.append(
122
+ ReliabilityBin(
123
+ lower=edges[i],
124
+ upper=edges[i + 1],
125
+ mean_confidence=mean_conf,
126
+ mean_outcome=mean_outcome,
127
+ count=len(b),
128
+ )
129
+ )
130
+ return out
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # calibration_adjustment — drives the shrinkage multiplier
135
+ # ---------------------------------------------------------------------------
136
+
137
+ #: Brier value at which we treat the agent as "well-calibrated" — boost above this.
138
+ WELL_CALIBRATED_BRIER = 0.10
139
+ #: Brier value above which we treat the agent as "overconfident" — shrink hard.
140
+ OVERCONFIDENT_BRIER = 0.25
141
+ #: Multiplier bounds — never zero (pure shrinkage) or unbounded (pure boost).
142
+ ADJ_MIN = 0.5
143
+ ADJ_MAX = 2.0
144
+ #: Minimum observations before we adjust at all.
145
+ MIN_OBSERVATIONS = 5
146
+
147
+
148
+ def calibration_adjustment(mean_brier: float | None, n_observations: int) -> float:
149
+ """Map a mean Brier score to a multiplicative effective_n adjustment.
150
+
151
+ Returns 1.0 (no adjustment) if there are too few observations. Otherwise:
152
+
153
+ mean_brier ≤ 0.10 → boost (multiplier > 1.0, up to 2.0)
154
+ mean_brier ≥ 0.25 → shrink (multiplier < 1.0, down to 0.5)
155
+ between → linear interpolation
156
+
157
+ Tuning rationale:
158
+ - Brier ≤ 0.10 is what well-calibrated forecasters achieve on
159
+ binary questions in academic studies (e.g. Tetlock's superforecasters).
160
+ - Brier ≥ 0.25 is roughly worse than random for a 50/50 question
161
+ (Brier of a fair coin on a 50/50 → 0.25), so the agent's
162
+ confidence is not informative and should be down-weighted.
163
+ """
164
+ if mean_brier is None or n_observations < MIN_OBSERVATIONS:
165
+ return 1.0
166
+
167
+ b = max(0.0, float(mean_brier))
168
+ if b <= WELL_CALIBRATED_BRIER:
169
+ # Map [0.0, 0.10] → [ADJ_MAX, 1.0]
170
+ frac = b / WELL_CALIBRATED_BRIER
171
+ return ADJ_MAX - frac * (ADJ_MAX - 1.0)
172
+ if b >= OVERCONFIDENT_BRIER:
173
+ return ADJ_MIN
174
+ # Linear from 1.0 (at b=0.10) to ADJ_MIN (at b=0.25)
175
+ span = OVERCONFIDENT_BRIER - WELL_CALIBRATED_BRIER
176
+ frac = (b - WELL_CALIBRATED_BRIER) / span
177
+ return 1.0 - frac * (1.0 - ADJ_MIN)
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # BrierScoreTracker
182
+ # ---------------------------------------------------------------------------
183
+
184
+ #: Cap on per-(agent, topic) history length to bound memory.
185
+ MAX_HISTORY_PER_KEY = 200
186
+
187
+
188
+ class BrierScoreTracker:
189
+ """Per-(agent, topic) Brier-score history and aggregates.
190
+
191
+ Lives on :class:`ReputationKernel` (one tracker per kernel) so
192
+ every oracle resolution can call :meth:`record` without plumbing.
193
+ """
194
+
195
+ def __init__(self, *, max_history_per_key: int = MAX_HISTORY_PER_KEY) -> None:
196
+ self.max_history_per_key = max_history_per_key
197
+ self._history: dict[tuple[bytes, str], list[BrierEntry]] = {}
198
+
199
+ # ------------------------------------------------------------------
200
+ # Write path
201
+ # ------------------------------------------------------------------
202
+
203
+ def record(
204
+ self,
205
+ agent_kid: bytes,
206
+ topic: str,
207
+ predicted_confidence: float,
208
+ outcome: float,
209
+ *,
210
+ ts_wall_ms: int | None = None,
211
+ ) -> BrierEntry:
212
+ """Append one observation. Returns the newly-built BrierEntry."""
213
+ key = (bytes(agent_kid), str(topic))
214
+ ts = int(time.time() * 1000) if ts_wall_ms is None else int(ts_wall_ms)
215
+ entry = BrierEntry(
216
+ ts_wall_ms=ts,
217
+ predicted_confidence=float(predicted_confidence),
218
+ outcome=float(outcome),
219
+ brier_score=brier_score(predicted_confidence, outcome),
220
+ )
221
+ bucket = self._history.setdefault(key, [])
222
+ bucket.append(entry)
223
+ if len(bucket) > self.max_history_per_key:
224
+ # Keep the most recent N
225
+ self._history[key] = bucket[-self.max_history_per_key :]
226
+ return entry
227
+
228
+ # ------------------------------------------------------------------
229
+ # Read path
230
+ # ------------------------------------------------------------------
231
+
232
+ def history(
233
+ self,
234
+ agent_kid: bytes,
235
+ topic: str,
236
+ *,
237
+ since_ms: int | None = None,
238
+ ) -> list[BrierEntry]:
239
+ rows = self._history.get((bytes(agent_kid), str(topic)), [])
240
+ if since_ms is None:
241
+ return list(rows)
242
+ return [r for r in rows if r.ts_wall_ms >= since_ms]
243
+
244
+ def mean_brier(
245
+ self,
246
+ agent_kid: bytes,
247
+ topic: str,
248
+ *,
249
+ since_ms: int | None = None,
250
+ ) -> float | None:
251
+ rows = self.history(agent_kid, topic, since_ms=since_ms)
252
+ if not rows:
253
+ return None
254
+ return sum(r.brier_score for r in rows) / len(rows)
255
+
256
+ def reliability_diagram(
257
+ self,
258
+ agent_kid: bytes,
259
+ topic: str,
260
+ *,
261
+ n_bins: int = 10,
262
+ ) -> list[ReliabilityBin]:
263
+ return reliability_diagram(
264
+ self.history(agent_kid, topic),
265
+ n_bins=n_bins,
266
+ )
267
+
268
+ def adjustment(self, agent_kid: bytes, topic: str) -> float:
269
+ """The shrinkage multiplier for this agent/topic — applied at consult time.
270
+
271
+ Returns 1.0 when there is insufficient history to adjust.
272
+ """
273
+ rows = self.history(agent_kid, topic)
274
+ if len(rows) < MIN_OBSERVATIONS:
275
+ return 1.0
276
+ m = sum(r.brier_score for r in rows) / len(rows)
277
+ return calibration_adjustment(m, len(rows))
278
+
279
+ def to_dict(self, agent_kid: bytes, topic: str) -> dict:
280
+ rows = self.history(agent_kid, topic)
281
+ return {
282
+ "agent_kid": bytes(agent_kid).hex(),
283
+ "topic": topic,
284
+ "n_observations": len(rows),
285
+ "mean_brier": self.mean_brier(agent_kid, topic),
286
+ "calibration_adjustment": self.adjustment(agent_kid, topic),
287
+ "reliability_diagram": [
288
+ b.to_dict() for b in self.reliability_diagram(agent_kid, topic)
289
+ ],
290
+ "history": [r.to_dict() for r in rows[-50:]], # tail
291
+ }
292
+
293
+
294
+ __all__ = [
295
+ "ADJ_MAX",
296
+ "ADJ_MIN",
297
+ "MAX_HISTORY_PER_KEY",
298
+ "MIN_OBSERVATIONS",
299
+ "OVERCONFIDENT_BRIER",
300
+ "WELL_CALIBRATED_BRIER",
301
+ "BrierEntry",
302
+ "BrierScoreTracker",
303
+ "ReliabilityBin",
304
+ "brier_score",
305
+ "calibration_adjustment",
306
+ "reliability_diagram",
307
+ ]