puremethod 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.
- app/__init__.py +2 -0
- app/__main__.py +31 -0
- app/_web/404/index.html +1 -0
- app/_web/404.html +1 -0
- app/_web/__next.!KGNvbnNvbGUp.__PAGE__.txt +42 -0
- app/_web/__next._full.txt +36 -0
- app/_web/__next._tree.txt +4 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_buildManifest.js +11 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_clientMiddlewareManifest.js +1 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_ssgManifest.js +1 -0
- app/_web/_next/static/chunks/00fylloju-1x2.js +1 -0
- app/_web/_next/static/chunks/03hxwfej4sm9g.js +1 -0
- app/_web/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- app/_web/_next/static/chunks/0gus9jro9n3j3.js +1 -0
- app/_web/_next/static/chunks/0n12a63ieedt9.js +1 -0
- app/_web/_next/static/chunks/0ns7paypbayry.js +31 -0
- app/_web/_next/static/chunks/0s1d7y-_wn8wn.js +1 -0
- app/_web/_next/static/chunks/12s40azz56b3b.js +1 -0
- app/_web/_next/static/chunks/1b_14wnek5geq.js +1 -0
- app/_web/_next/static/chunks/1oderptqhsaox.css +1 -0
- app/_web/_next/static/chunks/1rcnm-dleh7p6.js +1 -0
- app/_web/_next/static/chunks/1u4klctqx_w0w.js +1 -0
- app/_web/_next/static/chunks/1vahkxwiu_bqp.js +22 -0
- app/_web/_next/static/chunks/26mp4u1wew2xu.js +1 -0
- app/_web/_next/static/chunks/2aq880yo-2z88.js +1 -0
- app/_web/_next/static/chunks/2f5ovhdilzkal.js +1 -0
- app/_web/_next/static/chunks/2qds0qd18u74f.js +1 -0
- app/_web/_next/static/chunks/2qgmxvbiean_r.js +1 -0
- app/_web/_next/static/chunks/3b5v1mle2gmac.js +1 -0
- app/_web/_next/static/chunks/3hw8x3h98ysh4.js +6 -0
- app/_web/_next/static/chunks/3op5__grw4752.js +1 -0
- app/_web/_next/static/chunks/3scuzzds0vtp7.js +1 -0
- app/_web/_next/static/chunks/3v_5wdpvvcxz8.js +1 -0
- app/_web/_next/static/chunks/3wevuf_oyh97l.js +1 -0
- app/_web/_next/static/chunks/turbopack-3-ry682tkyiqk.js +1 -0
- app/_web/_not-found/__next._full.txt +24 -0
- app/_web/_not-found/__next._not-found.__PAGE__.txt +30 -0
- app/_web/_not-found/__next._tree.txt +4 -0
- app/_web/_not-found/index.html +1 -0
- app/_web/_not-found/index.txt +24 -0
- app/_web/credentials/__next.!KGNvbnNvbGUp.credentials.__PAGE__.txt +46 -0
- app/_web/credentials/__next._full.txt +39 -0
- app/_web/credentials/__next._tree.txt +5 -0
- app/_web/credentials/index.html +1 -0
- app/_web/credentials/index.txt +39 -0
- app/_web/executors/__next.!KGNvbnNvbGUp.executors.__PAGE__.txt +46 -0
- app/_web/executors/__next._full.txt +39 -0
- app/_web/executors/__next._tree.txt +5 -0
- app/_web/executors/index.html +1 -0
- app/_web/executors/index.txt +39 -0
- app/_web/index.html +1 -0
- app/_web/index.txt +36 -0
- app/_web/new/__next.!KGNvbnNvbGUp.new.__PAGE__.txt +46 -0
- app/_web/new/__next._full.txt +39 -0
- app/_web/new/__next._tree.txt +5 -0
- app/_web/new/index.html +1 -0
- app/_web/new/index.txt +39 -0
- app/_web/profiles/__next.!KGNvbnNvbGUp.profiles.__PAGE__.txt +46 -0
- app/_web/profiles/__next._full.txt +39 -0
- app/_web/profiles/__next._tree.txt +5 -0
- app/_web/profiles/index.html +1 -0
- app/_web/profiles/index.txt +39 -0
- app/_web/run/__next.!KGNvbnNvbGUp.run.__PAGE__.txt +47 -0
- app/_web/run/__next._full.txt +40 -0
- app/_web/run/__next._tree.txt +5 -0
- app/_web/run/index.html +1 -0
- app/_web/run/index.txt +40 -0
- app/_web/services/__next.!KGNvbnNvbGUp.services.__PAGE__.txt +46 -0
- app/_web/services/__next._full.txt +39 -0
- app/_web/services/__next._tree.txt +5 -0
- app/_web/services/index.html +1 -0
- app/_web/services/index.txt +39 -0
- app/_web/setup/__next.!KGNvbnNvbGUp.setup.__PAGE__.txt +46 -0
- app/_web/setup/__next._full.txt +39 -0
- app/_web/setup/__next._tree.txt +5 -0
- app/_web/setup/index.html +1 -0
- app/_web/setup/index.txt +39 -0
- app/_web/signin/__next._full.txt +30 -0
- app/_web/signin/__next._tree.txt +4 -0
- app/_web/signin/__next.signin.__PAGE__.txt +36 -0
- app/_web/signin/index.html +1 -0
- app/_web/signin/index.txt +30 -0
- app/accounts.py +137 -0
- app/addresses.py +30 -0
- app/api/__init__.py +61 -0
- app/api/auth_routes.py +54 -0
- app/api/common.py +102 -0
- app/api/config_routes.py +562 -0
- app/api/links.py +22 -0
- app/api/run_routes.py +311 -0
- app/api/setup_routes.py +267 -0
- app/bundled.py +521 -0
- app/compose.py +229 -0
- app/db.py +186 -0
- app/executor_config.py +95 -0
- app/keys.py +29 -0
- app/launcher.py +389 -0
- app/leases.py +40 -0
- app/literature_sources.py +131 -0
- app/model_endpoints.py +104 -0
- app/model_search.py +252 -0
- app/outputs.py +77 -0
- app/paths.py +93 -0
- app/readonly.py +70 -0
- app/repo.py +746 -0
- app/scan.py +65 -0
- app/server.py +24 -0
- app/settings.py +125 -0
- app/setup.py +117 -0
- app/ssh_chain.py +102 -0
- app/ssh_hosts.py +244 -0
- app/ssh_machines.py +101 -0
- app/statuses.py +4 -0
- app/supervisor.py +87 -0
- app/tunnels.py +222 -0
- app/unseal.py +27 -0
- app/worker.py +657 -0
- executor/__init__.py +47 -0
- executor/base.py +161 -0
- executor/common.py +139 -0
- executor/local_docker.py +239 -0
- executor/local_process.py +247 -0
- executor/ssh.py +481 -0
- executor/types.py +128 -0
- executor/watchdog.py +108 -0
- ledger/__init__.py +46 -0
- ledger/artifacts.py +62 -0
- ledger/canonical.py +48 -0
- ledger/core.py +337 -0
- ledger/errors.py +36 -0
- ledger/model.py +107 -0
- ledger/payloads.py +228 -0
- ledger/rules.py +204 -0
- ledger/storage.py +226 -0
- llm/__init__.py +45 -0
- llm/backends/__init__.py +0 -0
- llm/backends/anthropic_backend.py +145 -0
- llm/backends/base.py +87 -0
- llm/backends/openai_backend.py +211 -0
- llm/core.py +416 -0
- llm/prices.py +98 -0
- llm/tokens.py +18 -0
- llm/types.py +106 -0
- loop/__init__.py +19 -0
- loop/config.py +108 -0
- loop/core.py +1160 -0
- loop/decide.py +61 -0
- loop/harness.py +132 -0
- loop/prompts.py +88 -0
- loop/view.py +202 -0
- puremethod-0.1.0.dist-info/METADATA +79 -0
- puremethod-0.1.0.dist-info/RECORD +171 -0
- puremethod-0.1.0.dist-info/WHEEL +4 -0
- puremethod-0.1.0.dist-info/entry_points.txt +2 -0
- puremethod-0.1.0.dist-info/licenses/LICENSE +96 -0
- puremethod-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +173 -0
- purescholar/__init__.py +43 -0
- purescholar/client.py +380 -0
- purescholar/types.py +164 -0
- puretrace/__init__.py +49 -0
- puretrace/client.py +320 -0
- puretrace/types.py +166 -0
- writer/__init__.py +8 -0
- writer/core.py +430 -0
- writer/facts.py +162 -0
- writer/latex.py +62 -0
- writer/lint.py +85 -0
- writer/macros.py +114 -0
- writer/render.py +120 -0
- writer/tectonic.py +79 -0
- writer/types.py +61 -0
llm/prices.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Versioned price table (07-llm.md §4) and cost arithmetic.
|
|
2
|
+
|
|
3
|
+
Prices are USD per million tokens. A model with no entry is refused (INV-3). A local model
|
|
4
|
+
needs an explicit entry, which may be 0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .types import PriceUnknown, Usage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ModelPrice:
|
|
17
|
+
input: float
|
|
18
|
+
output: float
|
|
19
|
+
cache_read: float | None = None # None: charged at the input price (upper bound)
|
|
20
|
+
cache_write: float | None = None # None: charged at the input price
|
|
21
|
+
# Optional long-context tier: when input tokens exceed the threshold, the whole request
|
|
22
|
+
# is charged at these prices.
|
|
23
|
+
long_threshold: int | None = None
|
|
24
|
+
long_input: float | None = None
|
|
25
|
+
long_output: float | None = None
|
|
26
|
+
# A self-hosted model whose cost is not known per token (billed by the hour, or not
|
|
27
|
+
# known): calls count 0 against the money limit, are recorded `metered: false`, and need
|
|
28
|
+
# the run's unmetered-compute consent (INV-3, KB-036). Its input and output are 0.
|
|
29
|
+
unmetered: bool = False
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def unmetered_price() -> ModelPrice:
|
|
33
|
+
return ModelPrice(0.0, 0.0, unmetered=True)
|
|
34
|
+
|
|
35
|
+
def tier(self, input_tokens: int) -> ModelPrice:
|
|
36
|
+
if self.long_threshold is not None and input_tokens > self.long_threshold:
|
|
37
|
+
return replace(self, input=self.long_input or self.input,
|
|
38
|
+
output=self.long_output or self.output, long_threshold=None)
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class PriceTable:
|
|
44
|
+
version: str
|
|
45
|
+
source: str
|
|
46
|
+
prices: dict[tuple[str, str], ModelPrice] # (provider, model) -> price
|
|
47
|
+
|
|
48
|
+
def get(self, provider: str, model: str) -> ModelPrice:
|
|
49
|
+
price = self.prices.get((provider, model))
|
|
50
|
+
if price is None:
|
|
51
|
+
raise PriceUnknown(f"no price for {provider}/{model}; add one to the run config")
|
|
52
|
+
return price
|
|
53
|
+
|
|
54
|
+
def with_overrides(self, overrides: dict[str, dict[str, Any]]) -> PriceTable:
|
|
55
|
+
"""Overrides keyed `provider/model`, values are ModelPrice fields."""
|
|
56
|
+
prices = dict(self.prices)
|
|
57
|
+
for key, fields in overrides.items():
|
|
58
|
+
provider, _, model = key.partition("/")
|
|
59
|
+
if not model:
|
|
60
|
+
raise ValueError(f"price override key {key!r} must be provider/model")
|
|
61
|
+
prices[(provider, model)] = ModelPrice(**fields)
|
|
62
|
+
version = f"{self.version}+overrides" if overrides else self.version
|
|
63
|
+
return PriceTable(version=version, source=self.source, prices=prices)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cost_usd(price: ModelPrice, usage: Usage) -> float:
|
|
67
|
+
p = price.tier(usage.input_tokens + usage.cache_read_tokens + usage.cache_write_tokens)
|
|
68
|
+
cache_read = p.cache_read if p.cache_read is not None else p.input
|
|
69
|
+
cache_write = p.cache_write if p.cache_write is not None else p.input
|
|
70
|
+
return (usage.input_tokens * p.input + usage.output_tokens * p.output
|
|
71
|
+
+ usage.cache_read_tokens * cache_read
|
|
72
|
+
+ usage.cache_write_tokens * cache_write) / 1_000_000
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def bound_usd(price: ModelPrice, input_tokens: int, max_tokens: int) -> float:
|
|
76
|
+
"""Upper bound for one call: every input token at the highest input-side price."""
|
|
77
|
+
p = price.tier(input_tokens)
|
|
78
|
+
input_side = max(p.input, p.cache_read or 0.0, p.cache_write or 0.0)
|
|
79
|
+
return (input_tokens * input_side + max_tokens * p.output) / 1_000_000
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Source: Anthropic first-party API list prices for current models, as given in the
|
|
83
|
+
# claude-api reference (model table cached 2026-10-06), read 2026-10-27. Cache writes at
|
|
84
|
+
# 1.25x input (5-minute TTL). Claude Haiku 5.5: listed prices apply up to 100K prompt
|
|
85
|
+
# tokens; above that, $0.50 / $2.50. No OpenAI prices are shipped: they were not checked
|
|
86
|
+
# against a current source, so OpenAI models need a price in the run config (INV-3).
|
|
87
|
+
DEFAULT_PRICES = PriceTable(
|
|
88
|
+
version="2026-10-27.1",
|
|
89
|
+
source="Anthropic list prices via the claude-api reference, cached 2026-10-06",
|
|
90
|
+
prices={
|
|
91
|
+
("anthropic", "claude-fable-5-1"): ModelPrice(10.00, 50.00, 0.25, 12.50),
|
|
92
|
+
("anthropic", "claude-opus-5-5"): ModelPrice(4.00, 20.00, 0.20, 5.00),
|
|
93
|
+
("anthropic", "claude-sonnet-5-5"): ModelPrice(2.00, 10.00, 0.20, 2.50),
|
|
94
|
+
("anthropic", "claude-haiku-5-5"): ModelPrice(0.10, 0.50, None, 0.125,
|
|
95
|
+
long_threshold=100_000,
|
|
96
|
+
long_input=0.50, long_output=2.50),
|
|
97
|
+
},
|
|
98
|
+
)
|
llm/tokens.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Fallback token estimate (07-llm.md §5 step 2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
# Per-message allowance for role markers and framing, on top of the content estimate.
|
|
8
|
+
MESSAGE_OVERHEAD = 8
|
|
9
|
+
REQUEST_OVERHEAD = 16
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def estimate_tokens(text: str) -> int:
|
|
13
|
+
"""4 tokens per 3 characters, plus 10%, rounded up."""
|
|
14
|
+
return math.ceil(len(text) * 4 / 3 * 1.1)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def estimate_request(texts: list[str]) -> int:
|
|
18
|
+
return sum(estimate_tokens(t) + MESSAGE_OVERHEAD for t in texts) + REQUEST_OVERHEAD
|
llm/types.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Messages, results, call records and errors (07-llm.md §3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Literal, TypedDict
|
|
7
|
+
|
|
8
|
+
Role = Literal["planner", "coder", "analyst", "writer"]
|
|
9
|
+
ROLES: tuple[Role, ...] = ("planner", "coder", "analyst", "writer")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(TypedDict):
|
|
13
|
+
role: Literal["system", "user", "assistant"]
|
|
14
|
+
content: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Usage:
|
|
19
|
+
input_tokens: int = 0
|
|
20
|
+
output_tokens: int = 0
|
|
21
|
+
cache_read_tokens: int = 0
|
|
22
|
+
cache_write_tokens: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CallRecord:
|
|
27
|
+
"""One provider call, successful or not (INV-1). Same shape as the adapters' records.
|
|
28
|
+
|
|
29
|
+
`request` and `response` hold the full text for `loop` to store as ledger artifacts.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
source: str
|
|
33
|
+
role: str
|
|
34
|
+
provider: str
|
|
35
|
+
model: str
|
|
36
|
+
model_reported: str | None
|
|
37
|
+
fallback: bool
|
|
38
|
+
attempt: int
|
|
39
|
+
parameters: dict[str, Any]
|
|
40
|
+
started_at: str
|
|
41
|
+
ended_at: str
|
|
42
|
+
status: Literal["ok", "error"]
|
|
43
|
+
usage: Usage
|
|
44
|
+
cost_usd: float
|
|
45
|
+
price_table_version: str
|
|
46
|
+
bound_usd: float
|
|
47
|
+
bound_exceeded: bool
|
|
48
|
+
error_type: str | None
|
|
49
|
+
error_message: str | None
|
|
50
|
+
request: dict[str, Any]
|
|
51
|
+
response: str | None
|
|
52
|
+
metered: bool = True # False for a model priced as unmetered (INV-3, KB-036)
|
|
53
|
+
model_digest: str | None = None # as the server reports it, e.g. Ollama (§3, KB-036)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
d = dict(self.__dict__)
|
|
57
|
+
d["usage"] = dict(self.usage.__dict__)
|
|
58
|
+
return d
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class LLMResult:
|
|
63
|
+
text: str
|
|
64
|
+
parsed: Any
|
|
65
|
+
usage: Usage
|
|
66
|
+
cost_usd: float
|
|
67
|
+
model_reported: str | None
|
|
68
|
+
stop_reason: str | None
|
|
69
|
+
record: CallRecord
|
|
70
|
+
records: list[CallRecord] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LLMError(Exception):
|
|
74
|
+
"""Base error. `records` holds every call made before the failure (INV-1)."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, message: str, records: list[CallRecord] | None = None) -> None:
|
|
77
|
+
super().__init__(message)
|
|
78
|
+
self.records: list[CallRecord] = records or []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ProviderUnavailable(LLMError):
|
|
82
|
+
"""Outage or persistent transient failure. The caller pauses the run (SYS-INV-7)."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RateLimited(LLMError):
|
|
86
|
+
"""Rate limited after the retries ran out."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class QuotaExhausted(LLMError):
|
|
90
|
+
"""The account has no quota or credit left."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AuthError(LLMError):
|
|
94
|
+
"""Credentials missing, invalid, or not allowed."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InvalidOutput(LLMError):
|
|
98
|
+
"""The output still failed its schema after the retries."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PriceUnknown(LLMError):
|
|
102
|
+
"""The model has no price; it is refused rather than treated as free (INV-3)."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RequestRejected(LLMError):
|
|
106
|
+
"""The provider refused the request as malformed (HTTP 400, 404, 413, 422)."""
|
loop/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""loop: the research cycle (docs/knowledge_base/03-loop.md)."""
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
Autonomy,
|
|
5
|
+
ExecutorChoice,
|
|
6
|
+
LadderConfig,
|
|
7
|
+
Limits,
|
|
8
|
+
PaperConfig,
|
|
9
|
+
PureTraceUse,
|
|
10
|
+
RunConfig,
|
|
11
|
+
next_stage,
|
|
12
|
+
stage_budget_s,
|
|
13
|
+
)
|
|
14
|
+
from .core import LimitReached, Loop, RunStatus
|
|
15
|
+
from .decide import RULES, Verdict, evaluate
|
|
16
|
+
|
|
17
|
+
__all__ = ["RULES", "Autonomy", "ExecutorChoice", "LadderConfig", "LimitReached", "Limits",
|
|
18
|
+
"Loop", "PaperConfig", "PureTraceUse", "RunConfig", "RunStatus", "Verdict",
|
|
19
|
+
"evaluate", "next_stage", "stage_budget_s"]
|
loop/config.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""RunConfig (03-loop.md §3): validated once, then fixed for the run (INV-6)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
CHECKPOINTS = ("C1", "C2")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Strict(BaseModel):
|
|
13
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Limits(_Strict):
|
|
17
|
+
wall_clock: float = Field(gt=0, description="hours")
|
|
18
|
+
money: float = Field(gt=0, description="USD")
|
|
19
|
+
writing_reserve: float = Field(default=0.15, ge=0.05, le=0.5)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExecutorChoice(_Strict):
|
|
23
|
+
id: str = Field(min_length=1)
|
|
24
|
+
allow_unisolated: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LadderConfig(_Strict):
|
|
28
|
+
pilot_minutes: float = Field(default=5.0, gt=0)
|
|
29
|
+
growth_factor: float = Field(default=3.0, gt=1)
|
|
30
|
+
max_stages: int = Field(default=3, ge=1, le=6)
|
|
31
|
+
confirmation_seeds: int = Field(default=3, ge=1, le=10)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PureTraceUse(_Strict):
|
|
35
|
+
screening: bool = False
|
|
36
|
+
selfcheck: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PaperConfig(_Strict):
|
|
40
|
+
template: Literal["article", "ieeetran"] = "article"
|
|
41
|
+
disclosure: bool = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Autonomy(_Strict):
|
|
45
|
+
mode: Literal["autonomous", "checkpoint"] = "checkpoint"
|
|
46
|
+
checkpoints: tuple[str, ...] = CHECKPOINTS
|
|
47
|
+
|
|
48
|
+
@model_validator(mode="after")
|
|
49
|
+
def _known(self) -> Autonomy:
|
|
50
|
+
unknown = set(self.checkpoints) - set(CHECKPOINTS)
|
|
51
|
+
if unknown:
|
|
52
|
+
raise ValueError(f"unknown checkpoints: {sorted(unknown)}")
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RunConfig(_Strict):
|
|
57
|
+
idea: str = Field(min_length=1)
|
|
58
|
+
domain: Literal["ml", "simulation"]
|
|
59
|
+
autonomy: Autonomy = Autonomy()
|
|
60
|
+
limits: Limits
|
|
61
|
+
executor: ExecutorChoice
|
|
62
|
+
accept_unmetered_compute: bool = False
|
|
63
|
+
llm: dict[str, dict[str, Any]]
|
|
64
|
+
budget_ladder: LadderConfig = LadderConfig()
|
|
65
|
+
puretrace: PureTraceUse = PureTraceUse()
|
|
66
|
+
paper: PaperConfig = PaperConfig()
|
|
67
|
+
data_hints: tuple[str, ...] = ()
|
|
68
|
+
job_credentials: tuple[str, ...] = () # names the experiments may receive
|
|
69
|
+
|
|
70
|
+
@model_validator(mode="after")
|
|
71
|
+
def _roles(self) -> RunConfig:
|
|
72
|
+
missing = {"planner", "coder", "analyst"} - set(self.llm)
|
|
73
|
+
if missing:
|
|
74
|
+
raise ValueError(f"llm must map roles {sorted(missing)}")
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def checkpoint_on(self, cid: str) -> bool:
|
|
78
|
+
return self.autonomy.mode == "checkpoint" and cid in self.autonomy.checkpoints
|
|
79
|
+
|
|
80
|
+
def credential_names(self) -> list[str]:
|
|
81
|
+
"""Every credential the run uses, so its values are registered as secrets."""
|
|
82
|
+
names = {str(r["credential"]) for r in self.llm.values() if r.get("credential")}
|
|
83
|
+
for r in self.llm.values():
|
|
84
|
+
fb = r.get("fallback")
|
|
85
|
+
if isinstance(fb, dict) and fb.get("credential"):
|
|
86
|
+
names.add(str(fb["credential"]))
|
|
87
|
+
names.update(self.job_credentials)
|
|
88
|
+
return sorted(names)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def stage_budget_s(ladder: LadderConfig, stage: str) -> float:
|
|
92
|
+
"""Stage k gets pilot × growth^k; `confirm` gets the last stage's budget; `smoke` a
|
|
93
|
+
tenth of the pilot (at least 30 s)."""
|
|
94
|
+
pilot = ladder.pilot_minutes * 60
|
|
95
|
+
if stage == "smoke":
|
|
96
|
+
return max(pilot / 10, 30.0)
|
|
97
|
+
if stage == "confirm":
|
|
98
|
+
return pilot * ladder.growth_factor ** (ladder.max_stages - 1)
|
|
99
|
+
return pilot * ladder.growth_factor ** int(stage)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def next_stage(ladder: LadderConfig, stage: str) -> str:
|
|
103
|
+
if stage == "smoke":
|
|
104
|
+
return "0"
|
|
105
|
+
if stage == "confirm":
|
|
106
|
+
raise ValueError("confirm is the last stage")
|
|
107
|
+
k = int(stage) + 1
|
|
108
|
+
return "confirm" if k >= ladder.max_stages else str(k)
|