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,182 @@
1
+ """Plan / PlanDAG — typed plan nodes with Toulmin-shape edges.
2
+
3
+ A `PlanDAG` is a directed acyclic graph of `PlanNode`s. Edge kinds reuse
4
+ the Toulmin/IBIS shape from :mod:`hypermind.consult` (``supports``,
5
+ ``rebuts``, ``undercuts``, ``qualifies``) and add ``depends_on`` for
6
+ ordering — so a plan can both express data-flow precedence AND carry
7
+ the deliberation argument structure that supports/rebuts each step.
8
+
9
+ `Plan.to_workflow()` compiles a DAG into a runnable workflow body,
10
+ walking the topological order and issuing one ``wf.step`` per node.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Awaitable, Callable
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Literal
18
+
19
+ from hypermind.consult import ARGUMENT_EDGE_KINDS
20
+
21
+ PlanNodeKind = Literal["goal", "action", "branch", "wait", "fanout", "fanin"]
22
+ _VALID_PLAN_NODE_KINDS: frozenset[str] = frozenset(
23
+ {"goal", "action", "branch", "wait", "fanout", "fanin"}
24
+ )
25
+
26
+ # Plan edges extend Toulmin with ``depends_on`` (precedence) so the same
27
+ # edge type carries both data-flow and argument structure.
28
+ PlanEdgeKind = Literal["supports", "rebuts", "undercuts", "qualifies", "depends_on"]
29
+ _VALID_PLAN_EDGE_KINDS: frozenset[str] = frozenset(set(ARGUMENT_EDGE_KINDS) | {"depends_on"})
30
+
31
+
32
+ @dataclass
33
+ class PlanNode:
34
+ node_id: str
35
+ kind: PlanNodeKind
36
+ payload: dict[str, Any] = field(default_factory=dict)
37
+ depends_on: list[str] = field(default_factory=list)
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.kind not in _VALID_PLAN_NODE_KINDS:
41
+ raise ValueError(
42
+ f"unknown PlanNode kind {self.kind!r}; valid: {sorted(_VALID_PLAN_NODE_KINDS)}"
43
+ )
44
+
45
+
46
+ @dataclass
47
+ class PlanEdge:
48
+ src: str
49
+ dst: str
50
+ kind: PlanEdgeKind
51
+
52
+ def __post_init__(self) -> None:
53
+ if self.kind not in _VALID_PLAN_EDGE_KINDS:
54
+ raise ValueError(
55
+ f"unknown PlanEdge kind {self.kind!r}; valid: {sorted(_VALID_PLAN_EDGE_KINDS)}"
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class PlanDAG:
61
+ """A directed acyclic graph of plan nodes.
62
+
63
+ Cycles are rejected by :meth:`validate` (called automatically by
64
+ :meth:`topological`).
65
+ """
66
+
67
+ nodes: dict[str, PlanNode] = field(default_factory=dict)
68
+ edges: list[PlanEdge] = field(default_factory=list)
69
+
70
+ def add_node(self, node: PlanNode) -> None:
71
+ if node.node_id in self.nodes:
72
+ raise ValueError(f"duplicate node id {node.node_id!r}")
73
+ self.nodes[node.node_id] = node
74
+
75
+ def add_edge(
76
+ self,
77
+ src: str,
78
+ dst: str,
79
+ kind: PlanEdgeKind = "depends_on",
80
+ ) -> None:
81
+ if src not in self.nodes:
82
+ raise KeyError(f"unknown src node {src!r}")
83
+ if dst not in self.nodes:
84
+ raise KeyError(f"unknown dst node {dst!r}")
85
+ edge = PlanEdge(src=src, dst=dst, kind=kind)
86
+ self.edges.append(edge)
87
+ # Mirror depends_on into the node attr for convenience.
88
+ if kind == "depends_on":
89
+ node = self.nodes[dst]
90
+ if src not in node.depends_on:
91
+ node.depends_on.append(src)
92
+
93
+ def validate(self) -> None:
94
+ """Detect cycles via Kahn's algorithm; raise on failure."""
95
+ # Use only depends_on edges for ordering (Toulmin support/rebut
96
+ # edges are argument structure, not data flow).
97
+ indeg: dict[str, int] = {nid: 0 for nid in self.nodes}
98
+ adj: dict[str, list[str]] = {nid: [] for nid in self.nodes}
99
+ for e in self.edges:
100
+ if e.kind != "depends_on":
101
+ continue
102
+ indeg[e.dst] += 1
103
+ adj[e.src].append(e.dst)
104
+ queue = [nid for nid, d in indeg.items() if d == 0]
105
+ seen = 0
106
+ while queue:
107
+ n = queue.pop(0)
108
+ seen += 1
109
+ for m in adj[n]:
110
+ indeg[m] -= 1
111
+ if indeg[m] == 0:
112
+ queue.append(m)
113
+ if seen != len(self.nodes):
114
+ raise ValueError("PlanDAG has a cycle")
115
+
116
+ def topological(self) -> list[PlanNode]:
117
+ """Return nodes in a topological order (Kahn's, deterministic)."""
118
+ self.validate()
119
+ indeg: dict[str, int] = {nid: 0 for nid in self.nodes}
120
+ adj: dict[str, list[str]] = {nid: [] for nid in self.nodes}
121
+ for e in self.edges:
122
+ if e.kind != "depends_on":
123
+ continue
124
+ indeg[e.dst] += 1
125
+ adj[e.src].append(e.dst)
126
+ # Sort the zero-indegree frontier by node_id so iteration order
127
+ # is deterministic across runs (replay friendliness).
128
+ out: list[PlanNode] = []
129
+ queue = sorted([nid for nid, d in indeg.items() if d == 0])
130
+ while queue:
131
+ n = queue.pop(0)
132
+ out.append(self.nodes[n])
133
+ for m in sorted(adj[n]):
134
+ indeg[m] -= 1
135
+ if indeg[m] == 0:
136
+ queue.append(m)
137
+ queue.sort()
138
+ return out
139
+
140
+
141
+ @dataclass
142
+ class Plan:
143
+ """A PlanDAG plus an executor mapping (node_id → coroutine).
144
+
145
+ `to_workflow()` returns an async function whose body walks the DAG
146
+ in topological order and issues one ``wf.step`` per node. Callers
147
+ decorate the returned function with ``@workflow``.
148
+ """
149
+
150
+ dag: PlanDAG
151
+ executors: dict[str, Callable[..., Awaitable[Any]]] = field(default_factory=dict)
152
+ name: str = "plan"
153
+
154
+ def to_workflow(self) -> Callable[..., Awaitable[dict[str, Any]]]:
155
+ ordered = self.dag.topological()
156
+
157
+ async def _body(wf: Any) -> dict[str, Any]:
158
+ results: dict[str, Any] = {}
159
+ for node in ordered:
160
+ fn = self.executors.get(node.node_id)
161
+ if fn is None:
162
+ # Pure structural nodes (goal/branch/wait/fanout/fanin)
163
+ # without an executor become no-op steps that simply
164
+ # record their payload.
165
+ async def _passthrough(
166
+ payload: dict[str, Any] = node.payload,
167
+ ) -> dict[str, Any]:
168
+ return dict(payload)
169
+
170
+ results[node.node_id] = await wf.step(
171
+ f"{node.kind}:{node.node_id}",
172
+ _passthrough,
173
+ )
174
+ else:
175
+ results[node.node_id] = await wf.step(
176
+ f"{node.kind}:{node.node_id}",
177
+ fn,
178
+ )
179
+ return results
180
+
181
+ _body._hm_workflow_name = self.name # type: ignore[attr-defined]
182
+ return _body
@@ -0,0 +1,203 @@
1
+ """Plan-and-repair — replan a workflow when its claim is disputed.
2
+
3
+ A `RepairPolicy` examines a `WorkflowState` together with a
4
+ `DisputeOutcome` and produces a fresh `Plan` describing how to
5
+ recover. The default policy implements the spec §26 SHOULD-clauses:
6
+
7
+ * On ``UNRESOLVED_PARTITION``: retry from the last checkpoint with
8
+ widened evidence (re-run from the most recent successfully-completed
9
+ step that emitted a CHECKPOINT-marked record; if none, replan from
10
+ scratch).
11
+ * On ``CLOSED`` against the workflow's claim (i.e. counter-evidence
12
+ prevailed): emit a compensating plan that runs ``compensate()`` for
13
+ every committed action.
14
+
15
+ Wiring into Deliberator: :class:`RepairOnDispute` is an
16
+ :class:`ActionPolicy` subclass that fires when the agent observes a
17
+ ``dispute_close`` event whose target_kid matches a workflow-tagged
18
+ claim.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import Any, Protocol
25
+
26
+ from hypermind.mind.policy import (
27
+ ActionDecision,
28
+ ActionKind,
29
+ ActionPolicy,
30
+ PolicyContext,
31
+ )
32
+ from hypermind.workflow.plan import Plan, PlanDAG, PlanNode
33
+
34
+
35
+ class _DisputeOutcomeLike(Protocol):
36
+ """Structural shape — accepts the harness `DisputeOutcome`, the
37
+ simlab `DisputeOutcome`, or any duck-typed equivalent."""
38
+
39
+ final_state: str
40
+ target_kid: bytes
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class WorkflowDisputeContext:
45
+ """Bundle of inputs a repair policy reasons over."""
46
+
47
+ workflow_state: Any # WorkflowState (avoid import cycle)
48
+ outcome: _DisputeOutcomeLike
49
+ workflow_claim_kid: bytes | None = None
50
+
51
+
52
+ class RepairPolicy(Protocol):
53
+ """Decide how to recover from a workflow-affecting dispute close."""
54
+
55
+ def repair(self, ctx: WorkflowDisputeContext) -> Plan: ...
56
+
57
+
58
+ class DefaultRepairPolicy:
59
+ """Spec §26 default — retry-on-partition, compensate-on-close."""
60
+
61
+ def repair(self, ctx: WorkflowDisputeContext) -> Plan:
62
+ outcome = ctx.outcome
63
+ state_name = outcome.final_state
64
+ wf_state = ctx.workflow_state
65
+
66
+ dag = PlanDAG()
67
+ if state_name == "UNRESOLVED_PARTITION":
68
+ # Replan: start from the last completed checkpoint.
69
+ checkpoint_idx = _last_checkpoint_index(wf_state)
70
+ dag.add_node(
71
+ PlanNode(
72
+ node_id="replan_root",
73
+ kind="goal",
74
+ payload={
75
+ "rule_id": "WF-REPAIR-PARTITION-RETRY",
76
+ "from_step_idx": checkpoint_idx,
77
+ "widen_evidence": True,
78
+ },
79
+ )
80
+ )
81
+ dag.add_node(
82
+ PlanNode(
83
+ node_id="resume",
84
+ kind="action",
85
+ payload={
86
+ "rule_id": "WF-REPAIR-RESUME",
87
+ "workflow_id": getattr(wf_state, "workflow_id", ""),
88
+ },
89
+ depends_on=["replan_root"],
90
+ )
91
+ )
92
+ dag.add_edge("replan_root", "resume", "depends_on")
93
+ return Plan(dag=dag, name=f"repair-partition-{getattr(wf_state, 'workflow_id', '')}")
94
+
95
+ if state_name == "CLOSED":
96
+ # Compensating plan: one node per completed step in reverse.
97
+ dag.add_node(
98
+ PlanNode(
99
+ node_id="compensate_root",
100
+ kind="goal",
101
+ payload={"rule_id": "WF-REPAIR-COMPENSATE"},
102
+ )
103
+ )
104
+ prev = "compensate_root"
105
+ for i, step in enumerate(reversed(getattr(wf_state, "steps", []))):
106
+ if getattr(step, "status", "") != "COMPLETED":
107
+ continue
108
+ node_id = f"compensate_{i}"
109
+ dag.add_node(
110
+ PlanNode(
111
+ node_id=node_id,
112
+ kind="action",
113
+ payload={
114
+ "rule_id": "WF-REPAIR-COMPENSATE-STEP",
115
+ "step_name": step.step_name,
116
+ "step_id": step.step_id,
117
+ },
118
+ depends_on=[prev],
119
+ )
120
+ )
121
+ dag.add_edge(prev, node_id, "depends_on")
122
+ prev = node_id
123
+ return Plan(dag=dag, name=f"repair-close-{getattr(wf_state, 'workflow_id', '')}")
124
+
125
+ # Unknown final state — emit a single goal node so the caller
126
+ # at least sees a structured plan.
127
+ dag.add_node(
128
+ PlanNode(
129
+ node_id="noop",
130
+ kind="goal",
131
+ payload={
132
+ "rule_id": "WF-REPAIR-NOOP",
133
+ "final_state": state_name,
134
+ },
135
+ )
136
+ )
137
+ return Plan(dag=dag, name="repair-noop")
138
+
139
+
140
+ def _last_checkpoint_index(wf_state: Any) -> int:
141
+ """Return the index of the most recent step whose name starts with
142
+ ``checkpoint`` (case-insensitive). Returns 0 when none found."""
143
+ steps = getattr(wf_state, "steps", [])
144
+ for i, step in enumerate(reversed(steps)):
145
+ if "checkpoint" in step.step_name.lower():
146
+ return len(steps) - 1 - i
147
+ return 0
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # ActionPolicy hook for Deliberator
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ @dataclass
156
+ class RepairOnDispute(ActionPolicy):
157
+ """Emit an INTENT decision to replan when a workflow-tagged dispute closes.
158
+
159
+ The Deliberator passes a :class:`PolicyContext` whose
160
+ ``recent_observations`` may contain entries shaped::
161
+
162
+ {
163
+ "topic": ...,
164
+ "kind": "dispute_close",
165
+ "workflow_id": "abc123",
166
+ "final_state": "UNRESOLVED_PARTITION" | "CLOSED" | ...,
167
+ "target_kid": bytes,
168
+ }
169
+
170
+ On a match this policy returns an :class:`ActionDecision` with
171
+ ``kind=ActionKind.INTENT`` carrying ``rule_id="WF-REPAIR-INTENT"``
172
+ and the workflow_id + final_state in the payload. Downstream
173
+ machinery (out of scope for this policy class) is expected to call
174
+ :class:`DefaultRepairPolicy` to materialise the actual replan.
175
+ """
176
+
177
+ policy: RepairPolicy = None # type: ignore[assignment]
178
+ priority: int = 70
179
+
180
+ def __post_init__(self) -> None:
181
+ if self.policy is None:
182
+ self.policy = DefaultRepairPolicy()
183
+
184
+ def decide(self, ctx: PolicyContext) -> ActionDecision | None:
185
+ for obs in ctx.recent_observations:
186
+ if obs.get("kind") != "dispute_close":
187
+ continue
188
+ wid = obs.get("workflow_id")
189
+ if not wid:
190
+ continue
191
+ final_state = obs.get("final_state", "")
192
+ return ActionDecision(
193
+ kind=ActionKind.INTENT,
194
+ rule_id="WF-REPAIR-INTENT",
195
+ priority=self.priority,
196
+ payload={
197
+ "workflow_id": wid,
198
+ "final_state": final_state,
199
+ "target_kid": obs.get("target_kid"),
200
+ },
201
+ reason=f"workflow {wid} dispute closed in {final_state!r}",
202
+ )
203
+ return None