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.
Files changed (59) hide show
  1. outerloop/__init__.py +18 -0
  2. outerloop/__main__.py +3 -0
  3. outerloop/appauth.py +230 -0
  4. outerloop/appmanifest.py +203 -0
  5. outerloop/attempt.py +3784 -0
  6. outerloop/brief.py +528 -0
  7. outerloop/cli.py +621 -0
  8. outerloop/climbboard.py +1395 -0
  9. outerloop/compute.py +654 -0
  10. outerloop/contract.py +492 -0
  11. outerloop/contract_cli.py +63 -0
  12. outerloop/disk.py +164 -0
  13. outerloop/dispatch.py +631 -0
  14. outerloop/evalcache.py +147 -0
  15. outerloop/followup.py +2172 -0
  16. outerloop/github.py +1531 -0
  17. outerloop/harness.py +1435 -0
  18. outerloop/housekeeping.py +151 -0
  19. outerloop/image.py +368 -0
  20. outerloop/init.py +744 -0
  21. outerloop/intake.py +126 -0
  22. outerloop/launchlog.py +239 -0
  23. outerloop/limits.py +80 -0
  24. outerloop/maintain.py +353 -0
  25. outerloop/maintain_agent_cli.py +81 -0
  26. outerloop/maintain_post_cli.py +140 -0
  27. outerloop/markers.py +48 -0
  28. outerloop/measure.py +529 -0
  29. outerloop/orchestrator.py +2011 -0
  30. outerloop/panel.py +188 -0
  31. outerloop/paths.py +40 -0
  32. outerloop/posting.py +160 -0
  33. outerloop/progress.py +170 -0
  34. outerloop/py.typed +0 -0
  35. outerloop/review.py +615 -0
  36. outerloop/review_agent.py +263 -0
  37. outerloop/review_agent_cli.py +209 -0
  38. outerloop/review_post_cli.py +162 -0
  39. outerloop/review_summarize_cli.py +165 -0
  40. outerloop/role_runner.py +229 -0
  41. outerloop/roles.py +274 -0
  42. outerloop/rolespec.py +91 -0
  43. outerloop/runstate.py +385 -0
  44. outerloop/steward.py +845 -0
  45. outerloop/style.py +12 -0
  46. outerloop/syscall.py +1192 -0
  47. outerloop/syscall_cli.py +762 -0
  48. outerloop/tick.py +3422 -0
  49. outerloop/verifier.py +403 -0
  50. outerloop/verify_agent.py +151 -0
  51. outerloop/verify_agent_cli.py +95 -0
  52. outerloop/verify_post_cli.py +116 -0
  53. outerloop/watcher.py +203 -0
  54. outerloop_science-0.1.0.dist-info/METADATA +152 -0
  55. outerloop_science-0.1.0.dist-info/RECORD +59 -0
  56. outerloop_science-0.1.0.dist-info/WHEEL +4 -0
  57. outerloop_science-0.1.0.dist-info/entry_points.txt +2 -0
  58. outerloop_science-0.1.0.dist-info/licenses/LICENSE +202 -0
  59. 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"