consequa-x 0.1.0a2__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 (41) hide show
  1. consequa_x/__init__.py +22 -0
  2. consequa_x/__main__.py +3 -0
  3. consequa_x/adapters/__init__.py +5 -0
  4. consequa_x/adapters/ollama.py +350 -0
  5. consequa_x/attribution.py +407 -0
  6. consequa_x/benchmark/__init__.py +1 -0
  7. consequa_x/benchmark/assessment_metrics.py +54 -0
  8. consequa_x/benchmark/calibration.py +787 -0
  9. consequa_x/benchmark/calibration_export.py +139 -0
  10. consequa_x/benchmark/calibration_mechanisms.py +266 -0
  11. consequa_x/benchmark/decision_coalition.py +254 -0
  12. consequa_x/benchmark/decision_contrastive.py +448 -0
  13. consequa_x/benchmark/decision_replay.py +329 -0
  14. consequa_x/benchmark/decision_shapley.py +628 -0
  15. consequa_x/benchmark/evaluation.py +187 -0
  16. consequa_x/benchmark/interaction_export.py +220 -0
  17. consequa_x/benchmark/interaction_mechanisms.py +192 -0
  18. consequa_x/benchmark/interval_comparison.py +276 -0
  19. consequa_x/benchmark/intervals.py +164 -0
  20. consequa_x/benchmark/payments.py +426 -0
  21. consequa_x/benchmark/policy_experiment.py +298 -0
  22. consequa_x/benchmark/policy_runner.py +234 -0
  23. consequa_x/benchmark/runner.py +241 -0
  24. consequa_x/benchmark/study.py +183 -0
  25. consequa_x/benchmark/targets.py +149 -0
  26. consequa_x/benchmark/templates.py +102 -0
  27. consequa_x/cli.py +1191 -0
  28. consequa_x/explanation.py +130 -0
  29. consequa_x/graph.py +142 -0
  30. consequa_x/models.py +532 -0
  31. consequa_x/policies.py +248 -0
  32. consequa_x/py.typed +0 -0
  33. consequa_x/schema.py +159 -0
  34. consequa_x/schemas/trace-v0.1.json +543 -0
  35. consequa_x/verification.py +192 -0
  36. consequa_x-0.1.0a2.dist-info/METADATA +303 -0
  37. consequa_x-0.1.0a2.dist-info/RECORD +41 -0
  38. consequa_x-0.1.0a2.dist-info/WHEEL +5 -0
  39. consequa_x-0.1.0a2.dist-info/entry_points.txt +2 -0
  40. consequa_x-0.1.0a2.dist-info/licenses/LICENSE +21 -0
  41. consequa_x-0.1.0a2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,139 @@
1
+ """Portable, deterministic evidence exports for the offline calibration probe."""
2
+
3
+ from contextlib import ExitStack
4
+ import gzip
5
+ from hashlib import sha256
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from .. import __version__
10
+ from ..models import to_dict
11
+ from ..schema import get_trace_schema
12
+ from .calibration import (
13
+ calibration_protocol, compact_calibration_trial, run_calibration,
14
+ )
15
+
16
+
17
+ def _json(value):
18
+ return json.dumps(to_dict(value), sort_keys=True, allow_nan=False, separators=(",", ":"))
19
+
20
+
21
+ def _write_json(path, value):
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ path.write_text(json.dumps(to_dict(value), indent=2, sort_keys=True, allow_nan=False) + "\n",
24
+ encoding="utf-8", newline="\n")
25
+
26
+
27
+ def export_calibration(plan, output, *, source_revision=None, on_trial=None):
28
+ """Save the frozen protocol first; retain every compact trial and replicate 0.
29
+
30
+ An existing nonempty directory is rejected. Partial exports survive an
31
+ execution or export error and lack the final completion manifest.
32
+ """
33
+ output = Path(output)
34
+ protocol = calibration_protocol(plan)
35
+ if on_trial is not None and not callable(on_trial):
36
+ raise TypeError("on_trial must be callable or None")
37
+ if source_revision is not None and (not isinstance(source_revision, str)
38
+ or not source_revision.strip()):
39
+ raise ValueError("source_revision must be a nonempty identifier or None")
40
+ if output.exists() and (not output.is_dir() or any(output.iterdir())):
41
+ raise ValueError("calibrate-attribution requires a new or empty output directory")
42
+ output.mkdir(parents=True, exist_ok=True)
43
+ _write_json(output / "protocol.json", {"package_version": __version__, **protocol,
44
+ "source_revision": source_revision,
45
+ "source_revision_trust": "caller supplied; the exporter does not inspect Git",
46
+ "retention": {"all_trials": "trials/<mechanism>.jsonl.gz",
47
+ "full_evidence_replicates": [0],
48
+ "selection": "replicate 0, every attempted branch, including failures",
49
+ "gzip_mtime": 0}})
50
+ _write_json(output / "trace-schema.json", get_trace_schema())
51
+
52
+ def gz_write(path, value):
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ with path.open("wb") as raw:
55
+ with gzip.GzipFile(filename="", fileobj=raw, mode="wb", mtime=0) as stream:
56
+ stream.write((_json(value) + "\n").encode("utf-8"))
57
+
58
+ def evidence(trial):
59
+ public = {"seed_plan": to_dict(trial.seed_plan), "methods": {}}
60
+ gold = {"seed_plan": to_dict(trial.seed_plan), "methods": {}}
61
+ for method, factual, decisions, experiment in (
62
+ ("contrastive", trial.factual, trial.factual_decisions, trial.contrastive),
63
+ ("shapley", trial.shapley_factual, trial.shapley_factual_decisions, trial.shapley),
64
+ ):
65
+ episodes = [("factual", factual, decisions)]
66
+ if experiment is not None:
67
+ for branch in experiment.branches:
68
+ index = branch.index if method == "contrastive" else branch.slot.index
69
+ episodes.append((f"branch_{index}", branch.replay.episode,
70
+ branch.replay.decisions))
71
+ public["methods"][method] = {}
72
+ gold["methods"][method] = {}
73
+ for key, episode, receipts in episodes:
74
+ record = {"decisions": to_dict(receipts)}
75
+ if episode is not None:
76
+ record.update(trace=to_dict(episode.trace),
77
+ explanation=episode.explanation.to_dict())
78
+ gold["methods"][method][key] = {
79
+ "config": to_dict(episode.config), "seed": episode.seed,
80
+ "records": to_dict(episode.ground_truth)}
81
+ public["methods"][method][key] = record
82
+ gz_write(output / "evidence" / f"{trial.seed_plan.mechanism}.json.gz", public)
83
+ gz_write(output / "evaluator_truth" / f"{trial.seed_plan.mechanism}.json.gz", gold)
84
+
85
+ with ExitStack() as stack:
86
+ streams = {}
87
+ for name in plan.mechanisms:
88
+ path = output / "trials" / f"{name}.jsonl.gz"
89
+ path.parent.mkdir(parents=True, exist_ok=True)
90
+ raw = stack.enter_context(path.open("wb"))
91
+ streams[name] = stack.enter_context(gzip.GzipFile(
92
+ filename="", fileobj=raw, mode="wb", mtime=0))
93
+
94
+ def retain(trial):
95
+ record = compact_calibration_trial(trial).to_dict()
96
+ streams[trial.seed_plan.mechanism].write((_json(record) + "\n").encode("utf-8"))
97
+ if trial.seed_plan.replicate == 0:
98
+ evidence(trial)
99
+ if on_trial is not None:
100
+ on_trial(trial)
101
+ if trial.seed_plan.replicate == plan.repeats - 1:
102
+ print(f"Completed {trial.seed_plan.mechanism}: {plan.repeats} planned trials",
103
+ flush=True)
104
+
105
+ result = run_calibration(plan, on_trial=retain)
106
+
107
+ summary = result.summary()
108
+ _write_json(output / "summary.json", {"package_version": __version__, **summary})
109
+ report = (
110
+ "# Synthetic attribution calibration probe\n\n"
111
+ f"{len(result.trials)} predeclared trials across {len(plan.mechanisms)} mechanisms; "
112
+ f"{plan.repeats} repetitions each. No language model was called.\n\n"
113
+ "`protocol.json` was saved before policy calls. `summary.json` reports "
114
+ "availability, reference eligibility, error and interval coverage with explicit "
115
+ "denominators. All planned draws and refusal receipts are in compressed JSONL "
116
+ "files under `trials/`. Full public traces, explanations and decision receipts "
117
+ "for replicate 0 are in `evidence/`; its gold records stay separately in "
118
+ "`evaluator_truth/`. Python's standard `gzip` module can read these files.\n\n"
119
+ "The policy has a declared fixed factual timeout intervention and stochastic "
120
+ "fresh continuations. Analytic references describe its ideal Bernoulli law, "
121
+ "not the observed replay sample. The three-position retry game has undefined "
122
+ "population Shapley values when some held positions are unreachable. Available "
123
+ "finite estimates there are not scored against fabricated truth.\n\n"
124
+ "Missing provenance, stale readback and the three-call budget control are "
125
+ "intentional refusal strata. Refusals remain in planned denominators. "
126
+ "State and linked targets reuse outcomes and are not independent replications. "
127
+ "This exploratory synthetic probe does not establish calibrated coverage, "
128
+ "model accuracy, interaction recovery or method superiority.\n\n"
129
+ "The file manifest detects changed bytes, not authenticity. The detailed "
130
+ "design and metric definitions are in `docs/attribution-calibration.md`.\n"
131
+ )
132
+ (output / "README.md").write_text(report, encoding="utf-8", newline="\n")
133
+ artifacts = [{"path": path.relative_to(output).as_posix(),
134
+ "sha256": sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size}
135
+ for path in sorted(output.rglob("*")) if path.is_file()]
136
+ _write_json(output / "manifest.json", {"package_version": __version__,
137
+ "status": "completed", "artifacts": artifacts})
138
+ print(f"Saved calibration probe in {output.resolve()}")
139
+ return result
@@ -0,0 +1,266 @@
1
+ """Predeclared synthetic refund mechanisms with analytic population references.
2
+
3
+ The factual timeout choice is a benchmark intervention at one declared phase
4
+ key, not a naturally sampled factual trajectory. Every other timeout choice
5
+ uses a seeded Bernoulli retry law. Policies read public requests only; the
6
+ reference below never observes a rollout, its labels, or its sampled seeds.
7
+ All effects still come from the ordinary isolated ``PaymentWorld`` runner.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ import math
14
+ import random
15
+ from types import MappingProxyType
16
+ from typing import Literal
17
+
18
+ from ..models import AssessmentStatus, ResponseStatus
19
+ from ..policies import PolicyDecision, PolicyError, PolicyReply, PolicyRequest, validate_seed
20
+ from .payments import Fault
21
+ from .policy_runner import MAX_TRIAL_SEED, run_policy_scenario
22
+ from .runner import Episode, RefundContext, ScenarioConfig
23
+
24
+
25
+ TARGETS = ("action", "consequence", "action_linked_consequence")
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SeededRetryCalibrationPolicy:
30
+ """Transparent benchmark policy, including its reserved factual phase key.
31
+
32
+ Selection always refunds and the final response always stops. A timeout
33
+ with attempts remaining retries with probability ``retry_probability``
34
+ except at the exact reserved key, where the predeclared factual operation
35
+ is forced. The same policy identity must be used for factual and replay.
36
+ This conditioning is deliberately not a model or population factual draw.
37
+ """
38
+
39
+ retry_probability: float
40
+ factual_operation: Literal["stop", "retry"] = "stop"
41
+ factual_policy_seed: int = 0
42
+
43
+ def __post_init__(self) -> None:
44
+ _probability(self.retry_probability)
45
+ _operation(self.factual_operation)
46
+ _trial_seed(self.factual_policy_seed)
47
+
48
+ @property
49
+ def metadata(self) -> dict:
50
+ return {
51
+ "provider": "synthetic",
52
+ "policy": "conditioned-bernoulli-refund-calibration",
53
+ "version": "1",
54
+ "retry_probability": self.retry_probability,
55
+ "selection": "always_refund",
56
+ "conditioning": {
57
+ "kind": "predeclared_factual_timeout_intervention",
58
+ "naturally_sampled_factual": False,
59
+ "reserved_timeout_model_seed": self.factual_policy_seed * 6 + 1,
60
+ "operation": self.factual_operation,
61
+ },
62
+ "continuation_rng": "python_random.Random(model_phase_seed)",
63
+ "hidden_evidence_inputs": False,
64
+ }
65
+
66
+ def decide(self, request: PolicyRequest, *, seed: int) -> PolicyReply:
67
+ if not isinstance(request, PolicyRequest):
68
+ raise PolicyError("Expected a PolicyRequest")
69
+ validate_seed(seed)
70
+ # Only the public stage, remaining attempts and response status are used.
71
+ # No task fault, independent readback, or evaluator truth is supplied.
72
+ if request.stage == "select":
73
+ decision = PolicyDecision("refund", rationale="The fixture always selects the refund.")
74
+ source = "fixed_selection"
75
+ elif request.responses[-1].status is ResponseStatus.TIMEOUT and request.attempts_remaining:
76
+ forced = seed == self.factual_policy_seed * 6 + 1
77
+ retry = self.factual_operation == "retry" if forced else (
78
+ random.Random(seed).random() < self.retry_probability
79
+ )
80
+ decision = PolicyDecision("retry" if retry else "stop", AssessmentStatus.UNKNOWN,
81
+ "The fixture applies its declared timeout retry rule.")
82
+ source = "reserved_factual_intervention" if forced else "seeded_bernoulli_retry"
83
+ else:
84
+ decision = PolicyDecision("stop", AssessmentStatus.UNKNOWN,
85
+ "The fixture stops after the final or non-timeout response.")
86
+ source = "fixed_final_stop"
87
+ return PolicyReply(decision, {"seed": seed, "decision_source": source})
88
+
89
+
90
+ def _probability(value: float) -> None:
91
+ if type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1:
92
+ raise ValueError("retry_probability must be a finite probability in 0..1")
93
+
94
+
95
+ def _operation(value: str) -> None:
96
+ if type(value) is not str or value not in {"stop", "retry"}:
97
+ raise ValueError("factual_operation must be stop or retry")
98
+
99
+
100
+ def _trial_seed(value: int) -> None:
101
+ if type(value) is not int or not 0 <= value <= MAX_TRIAL_SEED:
102
+ raise ValueError(f"factual_policy_seed must be an integer in 0..{MAX_TRIAL_SEED}")
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class CalibrationMechanism:
107
+ """One world configuration and its independently specified retry law."""
108
+
109
+ name: str
110
+ retry_probability: float
111
+ factual_operation: Literal["stop", "retry"] = "stop"
112
+ use_idempotency: bool = False
113
+ include_provenance: bool = True
114
+ read_lag_ms: int = 0
115
+
116
+ def __post_init__(self) -> None:
117
+ if type(self.name) is not str or not self.name.strip():
118
+ raise ValueError("name must be a nonempty string")
119
+ _probability(self.retry_probability)
120
+ _operation(self.factual_operation)
121
+ if type(self.use_idempotency) is not bool or type(self.include_provenance) is not bool:
122
+ raise ValueError("use_idempotency and include_provenance must be booleans")
123
+ if type(self.read_lag_ms) is not int or self.read_lag_ms < 0:
124
+ raise ValueError("read_lag_ms must be a nonnegative integer")
125
+
126
+ @property
127
+ def decision_count(self) -> int:
128
+ return 2 if self.factual_operation == "stop" else 3
129
+
130
+ def make_policy(self, *, factual_policy_seed: int = 0) -> SeededRetryCalibrationPolicy:
131
+ return SeededRetryCalibrationPolicy(
132
+ self.retry_probability, self.factual_operation, factual_policy_seed,
133
+ )
134
+
135
+ def scenario_config(self) -> ScenarioConfig:
136
+ return ScenarioConfig(
137
+ name=self.name, fault=Fault.TIMEOUT_AFTER_COMMIT,
138
+ context=RefundContext(retry_instruction=True),
139
+ use_idempotency=self.use_idempotency,
140
+ include_provenance=self.include_provenance, read_lag_ms=self.read_lag_ms,
141
+ )
142
+
143
+ def capture_factual(self, *, seed: int = 0, policy_seed: int = 0) -> Episode:
144
+ """Convenience capture; budgeted callers should use the ordinary runner."""
145
+ return run_policy_scenario(
146
+ self.scenario_config(), self.make_policy(factual_policy_seed=policy_seed),
147
+ seed=seed, policy_seed=policy_seed, max_attempts=2,
148
+ )
149
+
150
+ def analytic_reference(self) -> dict:
151
+ """Describe the ideal Bernoulli game without running or reading episodes.
152
+
153
+ Coalition arrays use integer bit masks: bit ``i`` holds decision ``i``.
154
+ An undefined coalition retains ``None`` rather than a conditional mean
155
+ among valid rollouts. Retry factuals can stop before their held final
156
+ decision, so they have no complete Shapley population game for q < 1.
157
+ Endpoint truth remains available when both complete endpoints are known.
158
+ """
159
+ n, q = self.decision_count, self.retry_probability
160
+ retry_factual = self.factual_operation == "retry"
161
+ valid = [q if n == 3 and mask & 4 and not mask & 2 else 1.
162
+ for mask in range(1 << n)]
163
+ targets = {}
164
+ for target in TARGETS:
165
+ evidence_unknown = target != "action" and (
166
+ bool(self.read_lag_ms)
167
+ or (target == "action_linked_consequence" and not self.include_provenance)
168
+ )
169
+ # Without provenance, a duplicate still violates the state contract
170
+ # and has a known false linked target. The protective factual and
171
+ # held-stop coalitions nevertheless remain unknown.
172
+ factual_known_violation = retry_factual and not self.use_idempotency
173
+ factual_bad = (False if target == "action" else (
174
+ None if self.read_lag_ms else (
175
+ True if factual_known_violation else (None if evidence_unknown else False)
176
+ )
177
+ ))
178
+ numeric = not evidence_unknown
179
+ if target == "action":
180
+ changed = [0.] * n
181
+ coalition = [0. if probability == 1 else None for probability in valid]
182
+ elif numeric:
183
+ fresh_bad = 0. if self.use_idempotency else float(q)
184
+ changed = [fresh_bad, fresh_bad] + ([float(bool(factual_bad))] if n == 3 else [])
185
+ coalition = [
186
+ None if valid[mask] < 1 else (
187
+ float(bool(factual_bad)) if mask & 2 else fresh_bad
188
+ )
189
+ for mask in range(1 << n)
190
+ ]
191
+ else:
192
+ # With missing provenance, a fresh draw that necessarily
193
+ # duplicates is still a known violation. All other fresh laws
194
+ # have positive unknown probability and no binary mean.
195
+ fresh_bad = (1. if not self.read_lag_ms and not self.use_idempotency and q == 1
196
+ else None)
197
+ changed = [fresh_bad, fresh_bad] + (
198
+ [float(factual_bad) if factual_bad is not None else None] if n == 3 else []
199
+ )
200
+ coalition = [
201
+ None if valid[mask] < 1 else (
202
+ float(factual_bad) if factual_bad is not None else None
203
+ ) if mask & 2 else fresh_bad for mask in range(1 << n)
204
+ ]
205
+ effects = [None if rate is None or factual_bad is None else rate - int(factual_bad)
206
+ for rate in changed]
207
+ contrastive_eligible = factual_bad is not None and all(value is not None for value in effects)
208
+ shapley_eligible = all(value is not None for value in coalition)
209
+ endpoint_eligible = coalition[0] is not None and coalition[-1] is not None
210
+ if shapley_eligible:
211
+ # The initial selection is always the same operation; holding
212
+ # the timeout choice alone changes the population retry rate.
213
+ shares = [0., coalition[2] - coalition[0]] + ([0.] if n == 3 else [])
214
+ else:
215
+ shares = None
216
+ latest = max((i for i, effect in enumerate(effects) if effect is not None and effect < 0),
217
+ default=None) if contrastive_eligible else None
218
+ unknown_reason = "independent outcome evidence is unknown"
219
+ invalid_reason = "a fresh timeout stop can make held terminal decision 2 unreachable"
220
+ targets[target] = {
221
+ "factual_bad": factual_bad,
222
+ "changed_bad_rates": changed,
223
+ "contrastive_effects": effects,
224
+ "latest_population_rescue": latest,
225
+ "coalition_bad_rates": coalition,
226
+ "contrastive_eligible": contrastive_eligible,
227
+ "shapley_eligible": shapley_eligible,
228
+ "endpoint_eligible": endpoint_eligible,
229
+ "shapley_values": shares,
230
+ "endpoint_difference": coalition[-1] - coalition[0] if endpoint_eligible else None,
231
+ "contrastive_null_positions": [i for i, effect in enumerate(effects) if effect == 0],
232
+ "shapley_null_positions": [i for i, share in enumerate(shares or ()) if share == 0],
233
+ "reasons": {
234
+ "contrastive": None if contrastive_eligible else unknown_reason,
235
+ "shapley": None if shapley_eligible else (
236
+ invalid_reason if any(probability < 1 for probability in valid) else unknown_reason
237
+ ),
238
+ "endpoint": None if endpoint_eligible else unknown_reason,
239
+ },
240
+ }
241
+ return {
242
+ "name": self.name, "decision_count": n,
243
+ "retry_probability": q,
244
+ "conditioning": "fixed factual timeout intervention; ideal Bernoulli continuation law",
245
+ "population_not_finite_seed_claim": True,
246
+ "coalition_valid_probabilities": valid,
247
+ "targets": targets,
248
+ }
249
+
250
+
251
+ CALIBRATION_MECHANISMS = MappingProxyType({
252
+ **{f"protective-stop-q{int(q * 100)}": CalibrationMechanism(f"protective-stop-q{int(q * 100)}", q)
253
+ for q in (.1, .5, .9)},
254
+ **{f"retry-pivot-q{int(q * 100)}": CalibrationMechanism(f"retry-pivot-q{int(q * 100)}", q, "retry")
255
+ for q in (.1, .5, .9)},
256
+ "idempotent-stop-q50": CalibrationMechanism("idempotent-stop-q50", .5, use_idempotency=True),
257
+ "unknown-provenance-stop-q50": CalibrationMechanism("unknown-provenance-stop-q50", .5,
258
+ include_provenance=False),
259
+ "lagged-readback-stop-q50": CalibrationMechanism("lagged-readback-stop-q50", .5, read_lag_ms=1000),
260
+ })
261
+
262
+
263
+ def get_calibration_mechanism(name: str) -> CalibrationMechanism:
264
+ if type(name) is not str or name not in CALIBRATION_MECHANISMS:
265
+ raise ValueError(f"Unknown calibration mechanism: {name!r}")
266
+ return CALIBRATION_MECHANISMS[name]
@@ -0,0 +1,254 @@
1
+ """Replay positional coalitions of captured decisions in isolated worlds.
2
+
3
+ A held position forces its captured decision at the same typed policy stage.
4
+ Earlier fresh decisions may change its public request, so this intervention does
5
+ not require the exact request equality used by decision-prefix replay. An absent
6
+ held position invalidates a branch by default. The separate on-reach game
7
+ records normally terminated later held positions as inactive.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from copy import deepcopy
13
+ from dataclasses import dataclass
14
+ from typing import Iterable, Literal
15
+
16
+ from ..models import to_dict
17
+ from ..policies import PolicyError, PolicyReply, PolicyRequest, validate_reply
18
+ from .decision_replay import (
19
+ DecisionReplayReceipt, _Capture, _json_key, _metadata, _validate_factual,
20
+ )
21
+ from .evaluation import BudgetedPolicy, CallBudgetExceeded
22
+ from .policy_runner import MAX_TRIAL_SEED, run_policy_scenario
23
+ from .runner import Episode
24
+ from .templates import DEFAULT_REFUND_TEMPLATE, RefundTaskTemplate
25
+
26
+
27
+ COALITION_GAMES = ("strict", "on_reach")
28
+
29
+
30
+ def validate_coalition_game(game):
31
+ if type(game) is not str or game not in COALITION_GAMES:
32
+ raise ValueError("coalition_game must be strict or on_reach")
33
+
34
+
35
+ class DecisionCoalitionIncompatible(PolicyError):
36
+ """A forced factual decision is incompatible with its reached boundary."""
37
+
38
+
39
+ class DecisionCoalitionUnreachable(PolicyError):
40
+ """A completed rollout stopped before a required held position."""
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class DecisionCoalitionReplay:
45
+ """One coalition branch, including partial receipts after failure."""
46
+
47
+ held_indices: tuple[int, ...]
48
+ continuation_seed: int
49
+ factual_seed: int
50
+ factual_decision_count: int
51
+ policy_metadata: dict
52
+ decisions: tuple[DecisionReplayReceipt, ...]
53
+ policy_calls: int
54
+ episode: Episode | None = None
55
+ error_type: str | None = None
56
+ error: str | None = None
57
+ budget_exhausted: bool = False
58
+ coalition_game: str = "strict"
59
+
60
+ @property
61
+ def status(self) -> Literal["completed", "invalid", "failed"]:
62
+ if self.episode is not None:
63
+ return "completed"
64
+ if self.error_type in {
65
+ "DecisionCoalitionIncompatible", "DecisionCoalitionUnreachable",
66
+ }:
67
+ return "invalid"
68
+ return "failed"
69
+
70
+ @property
71
+ def unreached_factual_indices(self) -> tuple[int, ...]:
72
+ return tuple(range(min(len(self.decisions), self.factual_decision_count),
73
+ self.factual_decision_count))
74
+
75
+ @property
76
+ def unreached_held_indices(self) -> tuple[int, ...]:
77
+ return tuple(index for index in self.held_indices if index >= len(self.decisions))
78
+
79
+ @property
80
+ def inactive_held_indices(self) -> tuple[int, ...]:
81
+ """Only normal termination in the declared on-reach game is inactivity."""
82
+ return (self.unreached_held_indices
83
+ if self.coalition_game == "on_reach" and self.episode is not None else ())
84
+
85
+ def summary(self) -> dict:
86
+ return {
87
+ "status": self.status,
88
+ "coalition_game": self.coalition_game,
89
+ "inactive_held_indices": list(self.inactive_held_indices),
90
+ "held_indices": list(self.held_indices),
91
+ "continuation_seed": self.continuation_seed,
92
+ "factual_seed": self.factual_seed,
93
+ "factual_decision_count": self.factual_decision_count,
94
+ "reached_decisions": len(self.decisions),
95
+ "held_decisions": sum(item.source == "held_factual" and item.reply is not None
96
+ for item in self.decisions),
97
+ "resampled_decisions": sum(item.source == "resampled" and item.reply is not None
98
+ for item in self.decisions),
99
+ "held_requests": sum(item.source == "held_factual" for item in self.decisions),
100
+ "resampled_requests": sum(item.source == "resampled" for item in self.decisions),
101
+ "failed_decisions": sum(item.error_type is not None for item in self.decisions),
102
+ "unreached_factual_indices": list(self.unreached_factual_indices),
103
+ "unreached_held_indices": list(self.unreached_held_indices),
104
+ "policy_calls": self.policy_calls,
105
+ "budget_exhausted": self.budget_exhausted,
106
+ "error_type": self.error_type,
107
+ "error": self.error,
108
+ }
109
+
110
+
111
+ class _CoalitionPolicy:
112
+ def __init__(self, captures: tuple[_Capture, ...], policy: BudgetedPolicy,
113
+ held_indices: tuple[int, ...]):
114
+ self.captures = captures
115
+ self.policy = policy
116
+ self.held_indices = frozenset(held_indices)
117
+ self.receipts: list[DecisionReplayReceipt] = []
118
+ self._metadata = deepcopy(captures[0].policy_metadata)
119
+
120
+ @property
121
+ def metadata(self):
122
+ return deepcopy(self._metadata)
123
+
124
+ def decide(self, request: PolicyRequest, *, seed: int) -> PolicyReply:
125
+ index = len(self.receipts)
126
+ capture = self.captures[index] if index < len(self.captures) else None
127
+ source = "held_factual" if index in self.held_indices else "resampled"
128
+ before = self.policy.calls
129
+ request_data = to_dict(request)
130
+ reply = None
131
+ error = None
132
+ try:
133
+ if source == "held_factual":
134
+ if request.stage != capture.request.stage:
135
+ raise DecisionCoalitionIncompatible(
136
+ "Held decision reached a different typed policy stage",
137
+ )
138
+ reply = PolicyReply(deepcopy(capture.reply.decision), {
139
+ "decision_coalition_replay": {
140
+ "source": "forced_factual", "provider_call": False,
141
+ "factual_index": index,
142
+ "factual_event_id": capture.event_id,
143
+ "factual_model_seed": capture.model_seed,
144
+ "requested_continuation_model_seed": seed,
145
+ "exact_request_match": _json_key(request) == _json_key(capture.request),
146
+ "factual_reply_metadata": deepcopy(capture.reply.metadata),
147
+ },
148
+ })
149
+ try:
150
+ validate_reply(request, reply)
151
+ except (PolicyError, TypeError, ValueError) as caught:
152
+ raise DecisionCoalitionIncompatible(
153
+ f"Held decision is invalid for the current public request: {caught}",
154
+ ) from caught
155
+ else:
156
+ if _json_key(_metadata(self.policy)) != _json_key(self._metadata):
157
+ raise PolicyError("Continuation policy metadata differs from factual policy")
158
+ reply = self.policy.decide(deepcopy(request), seed=seed)
159
+ if _json_key(_metadata(self.policy)) != _json_key(self._metadata):
160
+ raise PolicyError("Continuation policy metadata changed during its decision")
161
+ validate_reply(request, reply)
162
+ reply = deepcopy(reply)
163
+ return reply
164
+ except (PolicyError, TypeError, ValueError) as caught:
165
+ error = caught
166
+ raise
167
+ finally:
168
+ self.receipts.append(DecisionReplayReceipt(
169
+ index, source, request.stage, seed,
170
+ capture.event_id if capture else None, capture.model_seed if capture else None,
171
+ self.policy.calls - before, request_data,
172
+ to_dict(reply) if reply is not None and error is None else None,
173
+ type(error).__name__ if error else None, str(error) if error else None,
174
+ ))
175
+
176
+
177
+ def replay_decision_coalition(
178
+ factual: Episode,
179
+ continuation_policy: BudgetedPolicy,
180
+ *,
181
+ held_indices: Iterable[int],
182
+ continuation_seed: int,
183
+ template: RefundTaskTemplate = DEFAULT_REFUND_TEMPLATE,
184
+ coalition_game: str = "strict",
185
+ ) -> DecisionCoalitionReplay:
186
+ """Reset the factual world and force the requested positional decisions.
187
+
188
+ Full factual captures and declared policy identity are validated without
189
+ provider calls before running a branch. Held decisions consume zero calls;
190
+ fresh decisions use a shared actual-call budget. New reached positions beyond
191
+ the factual trace are fresh. A branch may stop before unheld positions, but
192
+ stopping before a held position invalidates a strict-game branch. In the
193
+ explicit on-reach game, normally unreached held positions are inactive;
194
+ the completed episode and its verified outcome remain the actual rollout.
195
+ A reached but incompatible held decision is invalid under either game.
196
+
197
+ Forced decisions are positional interventions, including their captured
198
+ assessment and reported rationale. They need not be decisions the policy
199
+ would choose under a changed request. Same-stage and current-reply validity
200
+ checks keep this distinction explicit. Seeds do not establish model
201
+ independence or hidden-state identity.
202
+ """
203
+ if not isinstance(factual, Episode):
204
+ raise TypeError("factual must be an Episode")
205
+ validate_coalition_game(coalition_game)
206
+ if not isinstance(continuation_policy, BudgetedPolicy):
207
+ raise TypeError("continuation_policy must be a BudgetedPolicy")
208
+ if (type(continuation_policy.max_calls) is not int or continuation_policy.max_calls < 1
209
+ or type(continuation_policy.calls) is not int
210
+ or not 0 <= continuation_policy.calls <= continuation_policy.max_calls):
211
+ raise ValueError("BudgetedPolicy counters must be integers within its valid call limit")
212
+ if type(continuation_seed) is not int or not 0 <= continuation_seed <= MAX_TRIAL_SEED:
213
+ raise ValueError(f"continuation_seed must be an integer in 0..{MAX_TRIAL_SEED}")
214
+ if type(factual.seed) is not int or not 0 <= factual.seed <= MAX_TRIAL_SEED:
215
+ raise ValueError(f"factual seed must be an integer in 0..{MAX_TRIAL_SEED}")
216
+ if not isinstance(template, RefundTaskTemplate):
217
+ raise TypeError("template must be a RefundTaskTemplate")
218
+ try:
219
+ positions = tuple(held_indices)
220
+ except TypeError as caught:
221
+ raise TypeError("held_indices must be an iterable of decision indices") from caught
222
+ if any(type(index) is not int or index < 0 for index in positions):
223
+ raise ValueError("held_indices must contain nonnegative integers")
224
+ if len(set(positions)) != len(positions):
225
+ raise ValueError("held_indices must not contain duplicate positions")
226
+ held = tuple(sorted(positions))
227
+ captures = _validate_factual(factual, template)
228
+ if any(index >= len(captures) for index in held):
229
+ raise ValueError("held_indices exceed the factual captured decision range")
230
+ metadata = _metadata(continuation_policy)
231
+ if _json_key(metadata) != _json_key(captures[0].policy_metadata):
232
+ raise ValueError("Continuation policy metadata differs from factual policy")
233
+ coalition_policy = _CoalitionPolicy(captures, continuation_policy, held)
234
+ before = continuation_policy.calls
235
+ episode = None
236
+ error = None
237
+ try:
238
+ episode = run_policy_scenario(
239
+ factual.config, coalition_policy, seed=factual.seed, policy_seed=continuation_seed,
240
+ max_attempts=captures[0].request.attempts_remaining, template=template,
241
+ )
242
+ unreached = tuple(index for index in held if index >= len(coalition_policy.receipts))
243
+ if unreached and coalition_game == "strict":
244
+ raise DecisionCoalitionUnreachable(f"Held decision positions were not reached: {unreached}")
245
+ except (PolicyError, TypeError, ValueError) as caught:
246
+ error = caught
247
+ episode = None
248
+ return DecisionCoalitionReplay(
249
+ held, continuation_seed, factual.seed, len(captures), deepcopy(metadata),
250
+ tuple(coalition_policy.receipts), continuation_policy.calls - before, episode,
251
+ type(error).__name__ if error else None, str(error) if error else None,
252
+ isinstance(error, CallBudgetExceeded),
253
+ coalition_game,
254
+ )