Skip to content

Commit ba937d0

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): warm and retry Lean typechecks
Prewarm a reduced mathlib environment, classify 45/120-second typecheck retries, kill timed-out process groups, and stop whitespace-only model decode loops. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9ffe01d commit ba937d0

8 files changed

Lines changed: 319 additions & 33 deletions

File tree

KakeyaLeanGate/Prelude.lean

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import Mathlib
1+
import Mathlib.Analysis.Complex.Basic
2+
import Mathlib.Analysis.Complex.Hadamard
3+
import Mathlib.Analysis.Complex.JensenFormula
4+
import Mathlib.Analysis.Complex.LocallyUniformLimit
5+
import Mathlib.Analysis.Complex.Order
26

37
/-!
48
Minimal import target for AutoResearch theorem-signature validation.

autoresearch/prefill/lean_gate.py

Lines changed: 200 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
from __future__ import annotations
33

44
import hashlib
5+
import os
56
import re
7+
import signal
68
import subprocess
79
import tempfile
10+
import time
811
from dataclasses import dataclass
912
from pathlib import Path
1013

@@ -28,7 +31,19 @@ class LeanSignatureResult:
2831
source: str
2932
signature_hash: str
3033
ok: bool
34+
status: str = "FORMALIZED"
3135
error: str = ""
36+
attempts: int = 1
37+
elapsed_s: float = 0.0
38+
output: str = ""
39+
40+
41+
@dataclass(frozen=True)
42+
class _LeanRun:
43+
returncode: int | None
44+
timed_out: bool
45+
elapsed_s: float
46+
output: str
3247

3348

3449
def extract_lean_signature_blocks(text: str) -> list[tuple[str, str]]:
@@ -43,38 +58,159 @@ def _signature_only(source: str) -> str:
4358
return source[:match.start()].strip() if match else source.strip()
4459

4560

61+
def _run_lean(
62+
content: str,
63+
*,
64+
project_root: Path,
65+
timeout_s: float,
66+
) -> _LeanRun:
67+
started = time.monotonic()
68+
with tempfile.NamedTemporaryFile(
69+
mode="w",
70+
suffix=".lean",
71+
encoding="utf-8",
72+
delete=False,
73+
) as handle:
74+
handle.write(content)
75+
path = Path(handle.name)
76+
process = None
77+
try:
78+
process = subprocess.Popen(
79+
["lake", "env", "lean", str(path)],
80+
cwd=project_root,
81+
stdout=subprocess.PIPE,
82+
stderr=subprocess.STDOUT,
83+
text=True,
84+
start_new_session=True,
85+
)
86+
try:
87+
output, _ = process.communicate(timeout=timeout_s)
88+
return _LeanRun(
89+
process.returncode,
90+
False,
91+
time.monotonic() - started,
92+
output or "",
93+
)
94+
except subprocess.TimeoutExpired as exc:
95+
partial = (
96+
exc.stdout.decode(errors="replace")
97+
if isinstance(exc.stdout, bytes)
98+
else (exc.stdout or "")
99+
)
100+
try:
101+
os.killpg(process.pid, signal.SIGKILL)
102+
except (OSError, ProcessLookupError):
103+
process.kill()
104+
remainder, _ = process.communicate()
105+
return _LeanRun(
106+
None,
107+
True,
108+
time.monotonic() - started,
109+
partial + (remainder or ""),
110+
)
111+
except OSError as exc:
112+
return _LeanRun(
113+
None,
114+
False,
115+
time.monotonic() - started,
116+
f"{type(exc).__name__}: {exc}",
117+
)
118+
finally:
119+
if process is not None and process.poll() is None:
120+
process.kill()
121+
process.wait()
122+
path.unlink(missing_ok=True)
123+
124+
125+
def warm_lean_environment(
126+
project_root: Path,
127+
*,
128+
timeout_s: float = 120.0,
129+
) -> LeanSignatureResult:
130+
source = "theorem kakeyaLeanWarmup : True := by trivial"
131+
content = (
132+
"import KakeyaLeanGate\n\n"
133+
"set_option autoImplicit false\n\n"
134+
+ source
135+
+ "\n"
136+
)
137+
run = _run_lean(
138+
content,
139+
project_root=project_root,
140+
timeout_s=timeout_s,
141+
)
142+
if run.timed_out:
143+
return LeanSignatureResult(
144+
source,
145+
"",
146+
False,
147+
status="TYPECHECK_TIMEOUT",
148+
error=f"Lean warmup timed out after {timeout_s:.1f}s",
149+
elapsed_s=run.elapsed_s,
150+
output=run.output,
151+
)
152+
if run.returncode != 0:
153+
return LeanSignatureResult(
154+
source,
155+
"",
156+
False,
157+
status="ENVIRONMENT_FAILED",
158+
error=f"Lean warmup failed: {run.output[-2000:]}",
159+
elapsed_s=run.elapsed_s,
160+
output=run.output,
161+
)
162+
return LeanSignatureResult(
163+
source,
164+
"",
165+
True,
166+
status="ENVIRONMENT_READY",
167+
elapsed_s=run.elapsed_s,
168+
output=run.output,
169+
)
170+
171+
46172
def validate_lean_signature(
47173
source: str,
48174
*,
49175
project_root: Path,
50-
timeout_s: float = 30.0,
176+
timeout_s: float = 45.0,
177+
retry_timeout_s: float = 120.0,
51178
) -> LeanSignatureResult:
52179
source = source.strip()
53180
if not source:
54-
return LeanSignatureResult("", "", False, "empty Lean signature")
181+
return LeanSignatureResult(
182+
"", "", False, status="TYPECHECK_FAILED",
183+
error="empty Lean signature",
184+
)
55185
if len(source) > 12_000:
56-
return LeanSignatureResult("", "", False, "Lean signature too large")
186+
return LeanSignatureResult(
187+
"", "", False, status="TYPECHECK_FAILED",
188+
error="Lean signature too large",
189+
)
57190
if _FORBIDDEN.search(source):
58191
return LeanSignatureResult(
59192
source,
60193
"",
61194
False,
62-
"forbidden Lean command in generated signature",
195+
status="UNSAFE_REJECTED",
196+
error="forbidden Lean command in generated signature",
63197
)
64198
declarations = re.findall(r"^\s*theorem\s+([A-Za-z_][\w']*)", source, re.MULTILINE)
65199
if len(declarations) != 1:
66200
return LeanSignatureResult(
67201
source,
68202
"",
69203
False,
70-
"expected exactly one theorem declaration",
204+
status="TYPECHECK_FAILED",
205+
error="expected exactly one theorem declaration",
71206
)
72207
if not re.search(r"\s*:=\s*by\b", source):
73208
return LeanSignatureResult(
74209
source,
75210
"",
76211
False,
77-
"theorem signature must end with `:= by` proof scaffold",
212+
status="TYPECHECK_FAILED",
213+
error="theorem signature must end with `:= by` proof scaffold",
78214
)
79215
signature = " ".join(_signature_only(source).split())
80216
signature_hash = hashlib.sha256(signature.encode()).hexdigest()
@@ -84,39 +220,72 @@ def validate_lean_signature(
84220
+ source
85221
+ "\n"
86222
)
87-
try:
88-
with tempfile.NamedTemporaryFile(
89-
mode="w",
90-
suffix=".lean",
91-
encoding="utf-8",
92-
delete=False,
93-
) as handle:
94-
handle.write(content)
95-
path = Path(handle.name)
96-
completed = subprocess.run(
97-
["lake", "env", "lean", str(path)],
98-
cwd=project_root,
99-
capture_output=True,
100-
text=True,
101-
timeout=timeout_s,
102-
check=False,
223+
first = _run_lean(
224+
content,
225+
project_root=project_root,
226+
timeout_s=timeout_s,
227+
)
228+
attempts = 1
229+
total_elapsed = first.elapsed_s
230+
output = first.output
231+
run = first
232+
if first.timed_out:
233+
warmup = warm_lean_environment(
234+
project_root,
235+
timeout_s=retry_timeout_s,
103236
)
104-
except (OSError, subprocess.TimeoutExpired) as exc:
237+
total_elapsed += warmup.elapsed_s
238+
output += warmup.output
239+
if not warmup.ok:
240+
return LeanSignatureResult(
241+
source,
242+
signature_hash,
243+
False,
244+
status=warmup.status,
245+
error=warmup.error,
246+
attempts=1,
247+
elapsed_s=total_elapsed,
248+
output=output,
249+
)
250+
run = _run_lean(
251+
content,
252+
project_root=project_root,
253+
timeout_s=retry_timeout_s,
254+
)
255+
attempts = 2
256+
total_elapsed += run.elapsed_s
257+
output += run.output
258+
if run.timed_out:
105259
return LeanSignatureResult(
106260
source,
107261
signature_hash,
108262
False,
109-
f"Lean invocation failed: {type(exc).__name__}: {exc}",
263+
status="TYPECHECK_TIMEOUT",
264+
error=(
265+
f"Lean typecheck timed out after {attempts} attempts "
266+
f"({timeout_s:.1f}s/{retry_timeout_s:.1f}s)"
267+
),
268+
attempts=attempts,
269+
elapsed_s=total_elapsed,
270+
output=output,
110271
)
111-
finally:
112-
if "path" in locals():
113-
path.unlink(missing_ok=True)
114-
if completed.returncode != 0:
115-
error = (completed.stderr or completed.stdout).strip()
272+
if run.returncode != 0:
116273
return LeanSignatureResult(
117274
source,
118275
signature_hash,
119276
False,
120-
f"Lean typecheck failed: {error[-2000:]}",
277+
status="TYPECHECK_FAILED",
278+
error=f"Lean typecheck failed: {run.output[-2000:]}",
279+
attempts=attempts,
280+
elapsed_s=total_elapsed,
281+
output=output,
121282
)
122-
return LeanSignatureResult(source, signature_hash, True)
283+
return LeanSignatureResult(
284+
source,
285+
signature_hash,
286+
True,
287+
status="FORMALIZED",
288+
attempts=attempts,
289+
elapsed_s=total_elapsed,
290+
output=output,
291+
)

autoresearch/prefill/program.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ against pinned Lean/mathlib before persistence and records `FORMALIZED` plus a
9292
signature hash. Missing, unsafe, ill-typed, or duplicate signatures reject the
9393
child. `FORMALIZED` is not `PROVED`: closure still requires a separate proof
9494
with no `sorry` and no added axioms.
95+
The supervisor prewarms Lean. Signature checks use a 45-second first attempt;
96+
on timeout the entire Lean process group is killed, the environment is warmed
97+
again, and one 120-second retry is allowed. Distinguish `TYPECHECK_FAILED`,
98+
`TYPECHECK_TIMEOUT`, `UNSAFE_REJECTED`, and `ENVIRONMENT_FAILED`.
99+
100+
Generator/Critic decode must also make semantic progress. Three consecutive
101+
chunks containing only whitespace or empty decoded text terminate the turn as
102+
`semantic_stall`; never accept an unterminated partial Lean block.
95103

96104
Do not optimize output wording, scores, prizes, or other proof-irrelevant
97105
content. Prefill performance is a tertiary objective after mathematical

autoresearch/prefill/supervisor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from pathlib import Path
2020

2121
from autoresearch.prefill.prepare import _load_candidate, evaluate
22+
from autoresearch.prefill.lean_gate import warm_lean_environment
2223

2324

2425
REQUIRED_CANDIDATE_FIELDS = (
@@ -1315,6 +1316,18 @@ def main() -> int:
13151316
raise SystemExit("strategy-max-prefill-tokens must be > 0")
13161317
if args.strategy_stagnation_rounds <= 0:
13171318
raise SystemExit("strategy-stagnation-rounds must be > 0")
1319+
lean_warmup = warm_lean_environment(
1320+
Path(__file__).resolve().parents[2],
1321+
)
1322+
print(
1323+
"[autoresearch] phase=lean-warmup "
1324+
f"status={lean_warmup.status} "
1325+
f"elapsed_s={lean_warmup.elapsed_s:.2f} "
1326+
f"error={lean_warmup.error or '(none)'}",
1327+
flush=True,
1328+
)
1329+
if not lean_warmup.ok:
1330+
raise SystemExit(lean_warmup.error)
13181331
for iteration in range(args.iterations):
13191332
row = run_iteration(args, iteration)
13201333
print(json.dumps(row, indent=2, sort_keys=True))

scripts/agent_gan_inference_demo.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ def _infer(
6464
get_stats,
6565
on_token=None,
6666
max_response_tokens=None,
67+
semantic_progress=None,
68+
max_semantic_stall_chunks: int = 3,
6769
):
70+
if max_semantic_stall_chunks <= 0:
71+
raise ValueError("max_semantic_stall_chunks must be > 0")
6872
before = get_stats()
6973
started = time.perf_counter()
7074
with client.create_session(eos_token_ids=eos_ids, client_label="agent-gan") as s:
@@ -79,6 +83,7 @@ def _infer(
7983
else int(max_response_tokens) or None
8084
)
8185
stop_reason = "unknown"
86+
stalled_chunks = 0
8287
while response_limit is None or len(generated) < response_limit:
8388
before_count = len(generated)
8489
chunk = (
@@ -92,12 +97,21 @@ def _infer(
9297
on_token(generated)
9398
if first_at is None:
9499
first_at = time.perf_counter()
100+
new_tokens = generated[before_count:]
101+
if semantic_progress is not None and new_tokens:
102+
if semantic_progress(new_tokens):
103+
stalled_chunks = 0
104+
else:
105+
stalled_chunks += 1
95106
stop_reason = {
96107
1: "max_tokens",
97108
2: "eos",
98109
3: "cancelled",
99110
4: "truncated",
100111
}.get(s.last_stop_reason, "unknown")
112+
if stalled_chunks >= max_semantic_stall_chunks:
113+
stop_reason = "semantic_stall"
114+
break
101115
if stop_reason != "max_tokens":
102116
break
103117
if len(generated) == before_count:

0 commit comments

Comments
 (0)