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.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
conjecture_solver/llm.py
ADDED
|
@@ -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)
|