simjecture 0.1.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 (75) hide show
  1. conjecture_solver/__init__.py +10 -0
  2. conjecture_solver/__main__.py +4 -0
  3. conjecture_solver/action_handlers.py +459 -0
  4. conjecture_solver/adapters/__init__.py +7 -0
  5. conjecture_solver/adapters/base.py +96 -0
  6. conjecture_solver/adapters/fake.py +155 -0
  7. conjecture_solver/adapters/pic.py +196 -0
  8. conjecture_solver/adapters/warpx.py +1375 -0
  9. conjecture_solver/autonomous_research.py +1071 -0
  10. conjecture_solver/benchmarks/__init__.py +6 -0
  11. conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
  12. conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
  13. conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
  14. conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
  15. conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
  16. conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
  17. conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
  18. conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
  19. conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
  20. conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
  21. conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
  22. conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
  23. conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
  24. conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
  25. conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
  26. conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
  27. conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
  28. conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
  29. conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
  30. conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
  31. conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
  32. conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
  33. conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
  34. conjecture_solver/campaign.py +406 -0
  35. conjecture_solver/cli.py +963 -0
  36. conjecture_solver/confirmation.py +287 -0
  37. conjecture_solver/control.py +234 -0
  38. conjecture_solver/deployment.py +874 -0
  39. conjecture_solver/discovery.py +68 -0
  40. conjecture_solver/domains/__init__.py +40 -0
  41. conjecture_solver/domains/base.py +157 -0
  42. conjecture_solver/domains/kinetic_sufficiency.py +521 -0
  43. conjecture_solver/ledger.py +238 -0
  44. conjecture_solver/lifecycle.py +131 -0
  45. conjecture_solver/literature.py +494 -0
  46. conjecture_solver/llm.py +228 -0
  47. conjecture_solver/models.py +279 -0
  48. conjecture_solver/mvp_agent.py +3212 -0
  49. conjecture_solver/mvp_claims.py +1279 -0
  50. conjecture_solver/mvp_control.py +322 -0
  51. conjecture_solver/mvp_guidance.py +172 -0
  52. conjecture_solver/mvp_launch.py +1296 -0
  53. conjecture_solver/mvp_monitor.py +1487 -0
  54. conjecture_solver/mvp_skills.py +538 -0
  55. conjecture_solver/orchestration.py +578 -0
  56. conjecture_solver/outbox.py +526 -0
  57. conjecture_solver/parameters.py +394 -0
  58. conjecture_solver/proposals.py +427 -0
  59. conjecture_solver/research_tools.py +278 -0
  60. conjecture_solver/schema_export.py +246 -0
  61. conjecture_solver/search.py +1047 -0
  62. conjecture_solver/semantics.py +146 -0
  63. conjecture_solver/tui/__init__.py +7 -0
  64. conjecture_solver/tui/app.py +202 -0
  65. conjecture_solver/tui/claim_views.py +213 -0
  66. conjecture_solver/tui/screens.py +1014 -0
  67. conjecture_solver/warpx_analysis.py +184 -0
  68. conjecture_solver/warpx_campaign.py +148 -0
  69. conjecture_solver/warpx_confirmation.py +617 -0
  70. simjecture-0.1.0.dist-info/METADATA +400 -0
  71. simjecture-0.1.0.dist-info/RECORD +75 -0
  72. simjecture-0.1.0.dist-info/WHEEL +4 -0
  73. simjecture-0.1.0.dist-info/entry_points.txt +4 -0
  74. simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
  75. simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
@@ -0,0 +1,228 @@
1
+ """OpenAI-compatible provider client with explicit cheap/escalation routing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ DEFAULT_BASE_URL = "https://cp.compshare.cn/v1"
13
+ DEFAULT_MODEL = "deepseek-v4-flash-0731"
14
+ ESCALATION_MODEL = "glm-5.2"
15
+ DEEPSEEK_BASE_URL = "https://api.deepseek.com"
16
+ DEEPSEEK_DEFAULT_MODEL = "deepseek-v4-flash"
17
+ DEEPSEEK_ESCALATION_MODEL = "deepseek-v4-pro"
18
+
19
+
20
+ class ModelRoute(StrEnum):
21
+ DEFAULT = "default"
22
+ ESCALATION = "escalation"
23
+
24
+
25
+ class MissingCredential(RuntimeError):
26
+ pass
27
+
28
+
29
+ class EscalationReasonRequired(ValueError):
30
+ pass
31
+
32
+
33
+ class IncompleteCompletion(RuntimeError):
34
+ pass
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ModelSelection:
39
+ model: str
40
+ route: ModelRoute
41
+ reason: str
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class CompletionResult:
46
+ request_id: str
47
+ model: str
48
+ content: str
49
+ finish_reason: str
50
+ usage: dict[str, Any]
51
+ route: ModelRoute
52
+ route_reason: str
53
+
54
+
55
+ class ModelPolicy:
56
+ def __init__(
57
+ self,
58
+ *,
59
+ default_model: str = DEFAULT_MODEL,
60
+ escalation_model: str = ESCALATION_MODEL,
61
+ ) -> None:
62
+ self.default_model = default_model
63
+ self.escalation_model = escalation_model
64
+
65
+ @classmethod
66
+ def from_environment(cls) -> ModelPolicy:
67
+ return cls(
68
+ default_model=os.getenv("ACS_DEFAULT_MODEL", DEFAULT_MODEL),
69
+ escalation_model=os.getenv("ACS_ESCALATION_MODEL", ESCALATION_MODEL),
70
+ )
71
+
72
+ def select(
73
+ self,
74
+ route: ModelRoute = ModelRoute.DEFAULT,
75
+ *,
76
+ escalation_reason: str | None = None,
77
+ ) -> ModelSelection:
78
+ if route is ModelRoute.ESCALATION:
79
+ if not escalation_reason or not escalation_reason.strip():
80
+ raise EscalationReasonRequired(
81
+ "escalation routing requires a recorded escalation reason"
82
+ )
83
+ return ModelSelection(
84
+ model=self.escalation_model,
85
+ route=route,
86
+ reason=escalation_reason.strip(),
87
+ )
88
+ return ModelSelection(
89
+ model=self.default_model,
90
+ route=route,
91
+ reason="default low-cost reasoning route",
92
+ )
93
+
94
+
95
+ class OpenAICompatibleClient:
96
+ def __init__(
97
+ self,
98
+ *,
99
+ api_key: str,
100
+ base_url: str = DEFAULT_BASE_URL,
101
+ timeout_seconds: float = 120.0,
102
+ policy: ModelPolicy | None = None,
103
+ provider_name: str = "openai-compatible",
104
+ credential_source: str | None = None,
105
+ ) -> None:
106
+ if not api_key:
107
+ raise MissingCredential("API key is empty")
108
+ self._api_key = api_key
109
+ self.base_url = base_url.rstrip("/")
110
+ self.timeout_seconds = timeout_seconds
111
+ self.policy = policy or ModelPolicy.from_environment()
112
+ self.provider_name = provider_name
113
+ self.credential_source = credential_source
114
+
115
+ @classmethod
116
+ def from_environment(
117
+ cls,
118
+ *,
119
+ timeout_seconds: float | None = None,
120
+ ) -> OpenAICompatibleClient:
121
+ configured_provider = os.getenv("ACS_MODEL_PROVIDER", "").strip().casefold()
122
+ if configured_provider not in {"", "deepseek", "compshare"}:
123
+ raise ValueError(
124
+ "ACS_MODEL_PROVIDER must be 'deepseek' or 'compshare'"
125
+ )
126
+ deepseek_key = os.getenv("DEEPSEEK_API_KEY")
127
+ compshare_key = os.getenv("CP_API_KEY")
128
+ use_deepseek = configured_provider == "deepseek" or (
129
+ not configured_provider and bool(deepseek_key)
130
+ )
131
+ if use_deepseek:
132
+ if not deepseek_key:
133
+ raise MissingCredential(
134
+ "ACS_MODEL_PROVIDER=deepseek requires DEEPSEEK_API_KEY"
135
+ )
136
+ api_key = deepseek_key
137
+ base_url = os.getenv("DEEPSEEK_API_BASE_URL", DEEPSEEK_BASE_URL)
138
+ default_model = DEEPSEEK_DEFAULT_MODEL
139
+ escalation_model = DEEPSEEK_ESCALATION_MODEL
140
+ provider_name = "deepseek"
141
+ credential_source = "DEEPSEEK_API_KEY"
142
+ else:
143
+ if not compshare_key:
144
+ raise MissingCredential(
145
+ "set DEEPSEEK_API_KEY for the official DeepSeek API or "
146
+ "CP_API_KEY for the legacy CompShare route"
147
+ )
148
+ api_key = compshare_key
149
+ base_url = os.getenv("CP_API_BASE_URL", DEFAULT_BASE_URL)
150
+ default_model = DEFAULT_MODEL
151
+ escalation_model = ESCALATION_MODEL
152
+ provider_name = "compshare"
153
+ credential_source = "CP_API_KEY"
154
+ configured_timeout = float(os.getenv("ACS_MODEL_TIMEOUT_SECONDS", "300"))
155
+ return cls(
156
+ api_key=api_key,
157
+ base_url=base_url,
158
+ timeout_seconds=(
159
+ configured_timeout if timeout_seconds is None else timeout_seconds
160
+ ),
161
+ policy=ModelPolicy(
162
+ default_model=os.getenv("ACS_DEFAULT_MODEL", default_model),
163
+ escalation_model=os.getenv(
164
+ "ACS_ESCALATION_MODEL", escalation_model
165
+ ),
166
+ ),
167
+ provider_name=provider_name,
168
+ credential_source=credential_source,
169
+ )
170
+
171
+ def _headers(self) -> dict[str, str]:
172
+ return {
173
+ "Authorization": f"Bearer {self._api_key}",
174
+ "Content-Type": "application/json",
175
+ }
176
+
177
+ def list_models(self) -> tuple[str, ...]:
178
+ with httpx.Client(timeout=self.timeout_seconds) as client:
179
+ response = client.get(f"{self.base_url}/models", headers=self._headers())
180
+ response.raise_for_status()
181
+ payload = response.json()
182
+ return tuple(item["id"] for item in payload.get("data", ()) if "id" in item)
183
+
184
+ def complete(
185
+ self,
186
+ messages: list[dict[str, str]],
187
+ *,
188
+ route: ModelRoute = ModelRoute.DEFAULT,
189
+ escalation_reason: str | None = None,
190
+ max_tokens: int | None = None,
191
+ temperature: float = 0.0,
192
+ ) -> CompletionResult:
193
+ selection = self.policy.select(route, escalation_reason=escalation_reason)
194
+ request = {
195
+ "model": selection.model,
196
+ "messages": messages,
197
+ "temperature": temperature,
198
+ "stream": False,
199
+ }
200
+ if max_tokens is not None:
201
+ request["max_tokens"] = max_tokens
202
+ with httpx.Client(timeout=self.timeout_seconds) as client:
203
+ response = client.post(
204
+ f"{self.base_url}/chat/completions",
205
+ headers=self._headers(),
206
+ json=request,
207
+ )
208
+ response.raise_for_status()
209
+ payload = response.json()
210
+ choices = payload.get("choices") or []
211
+ if not choices:
212
+ raise IncompleteCompletion("provider returned no completion choice")
213
+ choice = choices[0]
214
+ content = (choice.get("message") or {}).get("content") or ""
215
+ if not content.strip():
216
+ raise IncompleteCompletion(
217
+ "provider returned no usable completion content"
218
+ )
219
+ finish_reason = choice.get("finish_reason") or "unknown"
220
+ return CompletionResult(
221
+ request_id=str(payload.get("id", "")),
222
+ model=str(payload.get("model", selection.model)),
223
+ content=content,
224
+ finish_reason=finish_reason,
225
+ usage=dict(payload.get("usage") or {}),
226
+ route=selection.route,
227
+ route_reason=selection.reason,
228
+ )
@@ -0,0 +1,279 @@
1
+ """Versioned scientific records shared by reasoning and deterministic tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import StrEnum
7
+ from typing import Any, Literal
8
+ from uuid import uuid4
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
11
+
12
+
13
+ def utc_now() -> datetime:
14
+ return datetime.now(UTC)
15
+
16
+
17
+ def new_id(prefix: str) -> str:
18
+ return f"{prefix}_{uuid4().hex}"
19
+
20
+
21
+ class StrictModel(BaseModel):
22
+ model_config = ConfigDict(
23
+ extra="forbid",
24
+ frozen=True,
25
+ str_strip_whitespace=True,
26
+ allow_inf_nan=False,
27
+ )
28
+
29
+
30
+ class PropositionClass(StrEnum):
31
+ EXISTENTIAL_CONTRIBUTION = "existential_contribution"
32
+ UNIVERSAL_NON_CONTRIBUTION = "universal_non_contribution"
33
+ PREDICTIVE_SUFFICIENCY = "predictive_sufficiency"
34
+ CAUSAL_EXHAUSTIVENESS = "causal_exhaustiveness"
35
+ INVARIANCE = "invariance"
36
+ THRESHOLD = "threshold"
37
+ PATH_DEPENDENCE = "path_dependence"
38
+
39
+
40
+ class HypothesisOrigin(StrEnum):
41
+ HUMAN = "human"
42
+ AI = "ai"
43
+ MIXED = "mixed"
44
+
45
+
46
+ class EvidenceRole(StrEnum):
47
+ DISCOVERY = "discovery"
48
+ CONFIRMATION = "confirmation"
49
+ QUALIFICATION = "qualification"
50
+
51
+
52
+ class AttemptOutcome(StrEnum):
53
+ SUCCESS = "success"
54
+ INFRASTRUCTURE_FAILURE = "infrastructure_failure"
55
+ SPECIFICATION_FAILURE = "specification_failure"
56
+ NUMERICAL_FAILURE = "numerical_failure"
57
+ VALIDITY_FAILURE = "validity_failure"
58
+ INTERRUPTED = "interrupted"
59
+
60
+
61
+ class ClaimDisposition(StrEnum):
62
+ SUPPORTED_WITHIN_MODEL = "supported_within_model"
63
+ REFUTED_WITHIN_MODEL = "refuted_within_model"
64
+ UNRESOLVED = "unresolved"
65
+
66
+
67
+ class CampaignStatus(StrEnum):
68
+ ACTIVE = "active"
69
+ PAUSE_REQUESTED = "pause_requested"
70
+ QUIESCING = "quiescing"
71
+ PAUSED = "paused"
72
+ RESUMING = "resuming"
73
+ RECOVERING = "recovering"
74
+ EMERGENCY_STOPPED = "emergency_stopped"
75
+ HUMAN_REVIEW = "human_review"
76
+ COMPLETED = "completed"
77
+ TERMINATED = "terminated"
78
+
79
+
80
+ class InterventionType(StrEnum):
81
+ PAUSE = "pause"
82
+ RESUME = "resume"
83
+ EMERGENCY_STOP = "emergency_stop"
84
+ ACTION_VETO = "action_veto"
85
+ PRIORITY_UPDATE = "priority_update"
86
+ TACTICAL_SUGGESTION = "tactical_suggestion"
87
+ REVIEWER_CHALLENGE = "reviewer_challenge"
88
+ EVIDENCE_INJECTION = "evidence_injection"
89
+ CONTRACT_AMENDMENT = "contract_amendment"
90
+ CAMPAIGN_BRANCH = "campaign_branch"
91
+
92
+
93
+ class DomainSpec(StrictModel):
94
+ schema_version: Literal["0.1.0"] = "0.1.0"
95
+ id: str = Field(default_factory=lambda: new_id("domain"))
96
+ description: str = Field(min_length=1)
97
+ model_family: str = Field(min_length=1)
98
+ assumptions: tuple[str, ...] = ()
99
+ fixed_parameters: dict[str, float | int | str] = Field(default_factory=dict)
100
+
101
+
102
+ class ObservableSpec(StrictModel):
103
+ schema_version: Literal["0.1.0"] = "0.1.0"
104
+ id: str = Field(default_factory=lambda: new_id("observable"))
105
+ name: str = Field(min_length=1)
106
+ semantic_kind: str = Field(min_length=1)
107
+ mathematical_definition: str = Field(min_length=1)
108
+ estimator: str = Field(min_length=1)
109
+ units: str = "dimensionless"
110
+ tolerance: float = Field(gt=0)
111
+
112
+
113
+ class EvidenceContract(StrictModel):
114
+ primary_observable_id: str
115
+ falsifying_witness: str = Field(min_length=1)
116
+ primary_tolerance: float = Field(gt=0)
117
+ minimum_independent_confirmation_attempts: int = Field(default=1, ge=1)
118
+
119
+
120
+ class MatchedPairFormalPredicate(StrictModel):
121
+ """Executable semantics for a finite predictive-sufficiency witness."""
122
+
123
+ schema_version: Literal["0.1.0"] = "0.1.0"
124
+ predicate_type: Literal["matched_pair_predictive_sufficiency"] = (
125
+ "matched_pair_predictive_sufficiency"
126
+ )
127
+ matched_coordinates: tuple[str, ...] = Field(min_length=1)
128
+ outcome_observable_id: str = Field(min_length=1)
129
+ coordinate_tolerance: float = Field(default=1e-10, ge=0)
130
+ maximum_outcome_difference: float = Field(gt=0)
131
+
132
+
133
+ class HypothesisNode(StrictModel):
134
+ schema_version: Literal["0.1.0"] = "0.1.0"
135
+ id: str = Field(default_factory=lambda: new_id("hypothesis"))
136
+ statement: str = Field(min_length=1)
137
+ machine_predicate: str = Field(min_length=1)
138
+ formal_predicate: MatchedPairFormalPredicate | None = None
139
+ proposition_class: PropositionClass
140
+ domain: DomainSpec
141
+ coordinates: tuple[str, ...]
142
+ evidence_contract: EvidenceContract
143
+ origin: HypothesisOrigin
144
+ parent_ids: tuple[str, ...] = ()
145
+ motivating_evidence_ids: tuple[str, ...] = ()
146
+
147
+ @model_validator(mode="after")
148
+ def require_declarative_statement(self) -> HypothesisNode:
149
+ if self.statement.endswith("?"):
150
+ raise ValueError("a hypothesis must be declarative, not a research question")
151
+ if not self.coordinates:
152
+ raise ValueError("a hypothesis must declare its tested coordinates")
153
+ if self.formal_predicate is not None:
154
+ if self.proposition_class is not PropositionClass.PREDICTIVE_SUFFICIENCY:
155
+ raise ValueError(
156
+ "the matched-pair predicate is only valid for predictive sufficiency"
157
+ )
158
+ if self.formal_predicate.matched_coordinates != self.coordinates:
159
+ raise ValueError("formal predicate coordinates must match hypothesis coordinates")
160
+ if (
161
+ self.formal_predicate.outcome_observable_id
162
+ != self.evidence_contract.primary_observable_id
163
+ ):
164
+ raise ValueError("formal predicate observable must match the evidence contract")
165
+ if (
166
+ abs(
167
+ self.formal_predicate.maximum_outcome_difference
168
+ - self.evidence_contract.primary_tolerance
169
+ )
170
+ > 1e-15
171
+ ):
172
+ raise ValueError("formal predicate tolerance must match the evidence contract")
173
+ return self
174
+
175
+
176
+ class ExperimentSpec(StrictModel):
177
+ schema_version: Literal["0.1.0"] = "0.1.0"
178
+ id: str = Field(default_factory=lambda: new_id("experiment"))
179
+ hypothesis_ids: tuple[str, ...]
180
+ action_type: str
181
+ physical_parameters: dict[str, float | int | str]
182
+ numerical_parameters: dict[str, float | int | str] = Field(default_factory=dict)
183
+ required_diagnostics: tuple[str, ...]
184
+ predictions: dict[str, str]
185
+ falsification_condition: str
186
+
187
+
188
+ class AttemptRecord(StrictModel):
189
+ schema_version: Literal["0.1.0"] = "0.1.0"
190
+ id: str = Field(default_factory=lambda: new_id("attempt"))
191
+ experiment_id: str
192
+ idempotency_key: str = Field(min_length=1)
193
+ outcome: AttemptOutcome | None = None
194
+ started_at: datetime = Field(default_factory=utc_now)
195
+ completed_at: datetime | None = None
196
+ external_job_id: str | None = None
197
+ failure_detail: str | None = None
198
+
199
+ @model_validator(mode="after")
200
+ def outcome_fields_are_consistent(self) -> AttemptRecord:
201
+ if self.outcome is None:
202
+ if self.completed_at or self.failure_detail:
203
+ raise ValueError("a pending attempt cannot be completed or failed")
204
+ return self
205
+ if self.completed_at is None:
206
+ raise ValueError("an attempt with an outcome requires completed_at")
207
+ failed = self.outcome is not AttemptOutcome.SUCCESS
208
+ if failed and not self.failure_detail:
209
+ raise ValueError("a non-successful attempt requires failure_detail")
210
+ if not failed and self.failure_detail:
211
+ raise ValueError("a successful attempt cannot carry failure_detail")
212
+ return self
213
+
214
+
215
+ class RunEvidence(StrictModel):
216
+ schema_version: Literal["0.1.0"] = "0.1.0"
217
+ id: str = Field(default_factory=lambda: new_id("evidence"))
218
+ source_attempt_id: str
219
+ role: EvidenceRole
220
+ eligible: bool
221
+ eligibility_reason: str
222
+ observable_values: dict[str, float]
223
+ uncertainties: dict[str, float] = Field(default_factory=dict)
224
+ independence_group: str
225
+ artifact_hashes: tuple[str, ...] = ()
226
+
227
+
228
+ class Claim(StrictModel):
229
+ schema_version: Literal["0.1.0"] = "0.1.0"
230
+ id: str = Field(default_factory=lambda: new_id("claim"))
231
+ hypothesis_id: str
232
+ statement: str
233
+ disposition: ClaimDisposition
234
+ scope: str
235
+ evidence_ids: tuple[str, ...] = Field(min_length=1)
236
+ limitations: tuple[str, ...] = ()
237
+ created_at: datetime = Field(default_factory=utc_now)
238
+
239
+
240
+ class DecisionRecord(StrictModel):
241
+ schema_version: Literal["0.1.0"] = "0.1.0"
242
+ id: str = Field(default_factory=lambda: new_id("decision"))
243
+ campaign_id: str
244
+ checkpoint_id: str
245
+ candidate_action_ids: tuple[str, ...]
246
+ selected_action_id: str
247
+ predicted_outcomes: dict[str, str]
248
+ validator_report: dict[str, Any]
249
+ model_id: str | None = None
250
+ model_route_reason: str | None = None
251
+ created_at: datetime = Field(default_factory=utc_now)
252
+
253
+
254
+ class CampaignCheckpoint(StrictModel):
255
+ schema_version: Literal["0.1.0"] = "0.1.0"
256
+ id: str = Field(default_factory=lambda: new_id("checkpoint"))
257
+ campaign_id: str
258
+ status: CampaignStatus
259
+ last_event_sequence: int = Field(ge=0)
260
+ state_hash: str
261
+ pending_action_ids: tuple[str, ...] = ()
262
+ external_job_ids: tuple[str, ...] = ()
263
+ budget_reserved: float = Field(default=0.0, ge=0)
264
+ component_versions: dict[str, str] = Field(default_factory=dict)
265
+ created_at: datetime = Field(default_factory=utc_now)
266
+
267
+
268
+ class HumanIntervention(StrictModel):
269
+ schema_version: Literal["0.1.0"] = "0.1.0"
270
+ id: str = Field(default_factory=lambda: new_id("intervention"))
271
+ actor: str
272
+ intervention_type: InterventionType
273
+ scope: str
274
+ reason: str
275
+ checkpoint_before: str
276
+ payload: dict[str, Any] = Field(default_factory=dict)
277
+ impact: dict[str, Any] = Field(default_factory=dict)
278
+ checkpoint_after: str | None = None
279
+ created_at: datetime = Field(default_factory=utc_now)