outerloop-science 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.
- outerloop/__init__.py +18 -0
- outerloop/__main__.py +3 -0
- outerloop/appauth.py +230 -0
- outerloop/appmanifest.py +203 -0
- outerloop/attempt.py +3784 -0
- outerloop/brief.py +528 -0
- outerloop/cli.py +621 -0
- outerloop/climbboard.py +1395 -0
- outerloop/compute.py +654 -0
- outerloop/contract.py +492 -0
- outerloop/contract_cli.py +63 -0
- outerloop/disk.py +164 -0
- outerloop/dispatch.py +631 -0
- outerloop/evalcache.py +147 -0
- outerloop/followup.py +2172 -0
- outerloop/github.py +1531 -0
- outerloop/harness.py +1435 -0
- outerloop/housekeeping.py +151 -0
- outerloop/image.py +368 -0
- outerloop/init.py +744 -0
- outerloop/intake.py +126 -0
- outerloop/launchlog.py +239 -0
- outerloop/limits.py +80 -0
- outerloop/maintain.py +353 -0
- outerloop/maintain_agent_cli.py +81 -0
- outerloop/maintain_post_cli.py +140 -0
- outerloop/markers.py +48 -0
- outerloop/measure.py +529 -0
- outerloop/orchestrator.py +2011 -0
- outerloop/panel.py +188 -0
- outerloop/paths.py +40 -0
- outerloop/posting.py +160 -0
- outerloop/progress.py +170 -0
- outerloop/py.typed +0 -0
- outerloop/review.py +615 -0
- outerloop/review_agent.py +263 -0
- outerloop/review_agent_cli.py +209 -0
- outerloop/review_post_cli.py +162 -0
- outerloop/review_summarize_cli.py +165 -0
- outerloop/role_runner.py +229 -0
- outerloop/roles.py +274 -0
- outerloop/rolespec.py +91 -0
- outerloop/runstate.py +385 -0
- outerloop/steward.py +845 -0
- outerloop/style.py +12 -0
- outerloop/syscall.py +1192 -0
- outerloop/syscall_cli.py +762 -0
- outerloop/tick.py +3422 -0
- outerloop/verifier.py +403 -0
- outerloop/verify_agent.py +151 -0
- outerloop/verify_agent_cli.py +95 -0
- outerloop/verify_post_cli.py +116 -0
- outerloop/watcher.py +203 -0
- outerloop_science-0.1.0.dist-info/METADATA +152 -0
- outerloop_science-0.1.0.dist-info/RECORD +59 -0
- outerloop_science-0.1.0.dist-info/WHEEL +4 -0
- outerloop_science-0.1.0.dist-info/entry_points.txt +2 -0
- outerloop_science-0.1.0.dist-info/licenses/LICENSE +202 -0
- outerloop_science-0.1.0.dist-info/licenses/NOTICE +5 -0
outerloop/evalcache.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""The seed cache: one uv cache per target, warmed by the kernel on the tick
|
|
2
|
+
host and copied into each job's own scratch cache before `uv` runs there
|
|
3
|
+
(docs/design/eval-cache.md). Jobs never write shared state: the warmer
|
|
4
|
+
downloads and unpacks wheels only (`--no-build`, so no build backend of the
|
|
5
|
+
target's ever runs here), and a job's copy is its own from the first byte."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
SEED_DIR = "eval-cache"
|
|
22
|
+
LOCK_HASH = ".lockfile-sha256"
|
|
23
|
+
# what uv sync needs to resolve a locked project without its sources
|
|
24
|
+
WARM_FILES = ("pyproject.toml", "uv.lock")
|
|
25
|
+
# the interpreter the eval jobs run under (the agent image's); wheels are
|
|
26
|
+
# picked for it, and the tick host's uv fetches it when it has none
|
|
27
|
+
EVAL_PYTHON = "3.12"
|
|
28
|
+
WARM_TIMEOUT_S = 30 * 60
|
|
29
|
+
# what uv needs to download wheels and nothing more: the tick host's
|
|
30
|
+
# environment holds credentials (token providers, key paths) that an
|
|
31
|
+
# uncontained download process has no business seeing
|
|
32
|
+
WARM_ENV_KEYS = (
|
|
33
|
+
"PATH",
|
|
34
|
+
"HOME",
|
|
35
|
+
"LANG",
|
|
36
|
+
"LC_ALL",
|
|
37
|
+
"TMPDIR",
|
|
38
|
+
"HTTP_PROXY",
|
|
39
|
+
"HTTPS_PROXY",
|
|
40
|
+
"NO_PROXY",
|
|
41
|
+
"http_proxy",
|
|
42
|
+
"https_proxy",
|
|
43
|
+
"no_proxy",
|
|
44
|
+
"SSL_CERT_FILE",
|
|
45
|
+
"SSL_CERT_DIR",
|
|
46
|
+
"REQUESTS_CA_BUNDLE",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
Runner = Callable[..., Any]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def seed_dir(root: Path, target: str) -> Path:
|
|
53
|
+
"""Where a target's seed lives under the state root."""
|
|
54
|
+
return root / SEED_DIR / target.replace("/", "__")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def lockfile_hash(files: dict[str, str]) -> str:
|
|
58
|
+
h = hashlib.sha256()
|
|
59
|
+
for name in sorted(files):
|
|
60
|
+
h.update(name.encode())
|
|
61
|
+
h.update(b"\0")
|
|
62
|
+
h.update(files[name].encode())
|
|
63
|
+
h.update(b"\0")
|
|
64
|
+
return h.hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def warm(
|
|
68
|
+
root: Path,
|
|
69
|
+
target: str,
|
|
70
|
+
github: Any,
|
|
71
|
+
ref: str,
|
|
72
|
+
*,
|
|
73
|
+
runner: Runner = subprocess.run,
|
|
74
|
+
uv: str | None = None,
|
|
75
|
+
python: str = EVAL_PYTHON,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Warm the target's seed from its lockfile at `ref`, when the lockfile
|
|
78
|
+
changed since the last warm. Fetches `pyproject.toml` and `uv.lock` only
|
|
79
|
+
(never the sources), and runs `uv sync --frozen --no-install-project
|
|
80
|
+
--no-build --all-extras` into a throwaway environment with the seed as
|
|
81
|
+
uv's cache. Returns one word on what happened, for the tick's report:
|
|
82
|
+
"warmed", "unchanged", or "skipped: ..." / "failed: ..." with the reason.
|
|
83
|
+
A failure leaves the seed as it was and the hash unrecorded, so the next
|
|
84
|
+
tick tries again."""
|
|
85
|
+
files: dict[str, str] = {}
|
|
86
|
+
for name in WARM_FILES:
|
|
87
|
+
text = github.get_file_content(target, name, ref)
|
|
88
|
+
if text is None:
|
|
89
|
+
return f"skipped: no {name} at {ref}"
|
|
90
|
+
files[name] = text
|
|
91
|
+
digest = lockfile_hash(files)
|
|
92
|
+
seed = seed_dir(root, target)
|
|
93
|
+
marker = seed / LOCK_HASH
|
|
94
|
+
try:
|
|
95
|
+
if marker.read_text().strip() == digest:
|
|
96
|
+
return "unchanged"
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
uv = uv or shutil.which("uv")
|
|
100
|
+
if not uv:
|
|
101
|
+
return "skipped: uv is not on the tick host's PATH"
|
|
102
|
+
# warm into a fresh directory beside the seed and swap it in whole: the
|
|
103
|
+
# seed then holds exactly this lockfile's wheels (nothing accumulates
|
|
104
|
+
# across lockfile changes), and a job copying mid-swap fails its
|
|
105
|
+
# best-effort copy rather than seeing a half-written cache
|
|
106
|
+
seed.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
fresh = seed.parent / f".{seed.name}.warm-{digest[:12]}"
|
|
108
|
+
shutil.rmtree(fresh, ignore_errors=True)
|
|
109
|
+
fresh.mkdir()
|
|
110
|
+
with tempfile.TemporaryDirectory(prefix="outerloop-warm-") as tmp:
|
|
111
|
+
for name, text in files.items():
|
|
112
|
+
(Path(tmp) / name).write_text(text)
|
|
113
|
+
env = {k: os.environ[k] for k in WARM_ENV_KEYS if k in os.environ}
|
|
114
|
+
env.update(
|
|
115
|
+
UV_CACHE_DIR=str(fresh),
|
|
116
|
+
UV_PROJECT_ENVIRONMENT=str(Path(tmp) / ".venv"),
|
|
117
|
+
UV_LINK_MODE="copy",
|
|
118
|
+
)
|
|
119
|
+
argv = [
|
|
120
|
+
uv,
|
|
121
|
+
"sync",
|
|
122
|
+
"--frozen",
|
|
123
|
+
"--no-install-project",
|
|
124
|
+
"--no-build",
|
|
125
|
+
"--all-extras",
|
|
126
|
+
"--python",
|
|
127
|
+
python,
|
|
128
|
+
]
|
|
129
|
+
try:
|
|
130
|
+
proc = runner(
|
|
131
|
+
argv, cwd=tmp, env=env, capture_output=True, text=True, timeout=WARM_TIMEOUT_S
|
|
132
|
+
)
|
|
133
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
134
|
+
shutil.rmtree(fresh, ignore_errors=True)
|
|
135
|
+
return f"failed: {type(exc).__name__}: {exc}"
|
|
136
|
+
if proc.returncode != 0:
|
|
137
|
+
shutil.rmtree(fresh, ignore_errors=True)
|
|
138
|
+
tail = (proc.stderr or "").strip().splitlines()[-3:]
|
|
139
|
+
return f"failed: uv sync exited {proc.returncode}: {' | '.join(tail)}"
|
|
140
|
+
(fresh / LOCK_HASH).write_text(digest)
|
|
141
|
+
old = seed.parent / f".{seed.name}.old"
|
|
142
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
143
|
+
if seed.exists():
|
|
144
|
+
os.replace(seed, old)
|
|
145
|
+
os.replace(fresh, seed)
|
|
146
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
147
|
+
return "warmed"
|