Skip to content

Commit 4f2eee3

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): parse prose strategy plans
Ignore LaTeX braces and convert structured natural-language plans into validated candidates so non-JSON Gemma output cannot stop the research loop. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 294b33a commit 4f2eee3

2 files changed

Lines changed: 101 additions & 7 deletions

File tree

autoresearch/prefill/supervisor.py

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import argparse
6+
import ast
67
import csv
78
import hashlib
89
import json
@@ -270,13 +271,72 @@ def render_candidate(candidate: dict) -> str:
270271

271272
def _extract_json(text: str) -> dict:
272273
stripped = text.strip()
273-
if stripped.startswith("```"):
274-
stripped = re.sub(r"^```(?:json)?\\s*", "", stripped)
275-
stripped = re.sub(r"\\s*```$", "", stripped)
276-
start, end = stripped.find("{"), stripped.rfind("}")
277-
if start < 0 or end <= start:
278-
raise ValueError("strategy agent returned no JSON object")
279-
return json.loads(stripped[start:end + 1])
274+
decoder = json.JSONDecoder()
275+
for start, character in enumerate(stripped):
276+
if character != "{":
277+
continue
278+
try:
279+
value, _ = decoder.raw_decode(stripped[start:])
280+
except json.JSONDecodeError:
281+
continue
282+
if isinstance(value, dict):
283+
value["strategy_parse_mode"] = "json"
284+
return value
285+
for block in re.findall(
286+
r"```(?:json|python)?\s*(.*?)```",
287+
stripped,
288+
re.DOTALL | re.IGNORECASE,
289+
):
290+
try:
291+
value = ast.literal_eval(block.strip())
292+
except (SyntaxError, ValueError):
293+
continue
294+
if isinstance(value, dict):
295+
value["strategy_parse_mode"] = "python-literal"
296+
return value
297+
target_matches = re.findall(
298+
r"(?:Targeting\s+Leaf|pending\s+leaf)\*{0,2}\s*:?\s*"
299+
r"(?:\*\*)?`?([A-Za-z0-9]+(?:-[A-Za-z0-9]+)+)`?(?:\*\*)?",
300+
stripped,
301+
re.IGNORECASE,
302+
)
303+
objective_match = re.search(
304+
r"Objective\*{0,2}\s*:\s*(.+)$",
305+
stripped,
306+
re.MULTILINE | re.IGNORECASE,
307+
)
308+
steps = [
309+
re.sub(r"^\s*(?:[-*]|\d+\.)\s*", "", line).strip()
310+
for line in stripped.splitlines()
311+
if re.match(r"^\s*(?:[-*]|\d+\.)\s+\S", line)
312+
and "Objective" not in line
313+
and "Constraint" not in line
314+
]
315+
objective = (
316+
objective_match.group(1).strip()
317+
if objective_match is not None
318+
else ""
319+
)
320+
if not objective:
321+
prose = [
322+
line.strip()
323+
for line in stripped.splitlines()
324+
if line.strip()
325+
and not line.strip().startswith(("#", "```"))
326+
]
327+
objective = " ".join(prose[:3])[:1200]
328+
if not objective:
329+
raise ValueError("strategy agent returned no usable candidate")
330+
digest = hashlib.sha256(stripped.encode()).hexdigest()[:12]
331+
return {
332+
"candidate_id": f"candidate-prose-{digest}",
333+
"target_obligation_id": (
334+
target_matches[-1] if target_matches else ""
335+
),
336+
"hypothesis": objective,
337+
"plan": {"steps": steps[:8]},
338+
"strategy_parse_mode": "prose",
339+
}
280340

281341

282342
def parse_research_verdict(output: str, candidate_id: str) -> dict:
@@ -498,6 +558,11 @@ def propose_candidate(
498558
flush=True,
499559
)
500560
candidate = _extract_json(strategy_output)
561+
parse_mode = candidate.pop("strategy_parse_mode", "unknown")
562+
print(
563+
f"[autoresearch] phase=strategy-parse mode={parse_mode}",
564+
flush=True,
565+
)
501566
candidate, repaired_fields = repair_candidate_schema(
502567
candidate,
503568
current=current,

tests/inference_engine/bench/test_autoresearch_supervisor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
render_candidate,
99
should_keep,
1010
StrategyPrefillHeartbeat,
11+
_extract_json,
1112
_pending_leaf_ids,
1213
validate_candidate,
1314
)
@@ -173,6 +174,34 @@ def test_strategy_schema_repair_flattens_nested_hypothesis_and_plan():
173174
}.issubset(set(fields))
174175

175176

177+
def test_strategy_parser_converts_prose_plan_and_ignores_latex_braces():
178+
output = r"""
179+
The next step is to address the pending leaf: **RH-C2-wrong**.
180+
181+
### Plan for Next Run
182+
1. **Objective**: Formulate the mathematical framework for the Zero-Exclusion Lemma.
183+
2. **Mathematical Strategy**:
184+
* Investigate $\tilde{F}_N(s)$ using Rouché's Theorem.
185+
* Determine conditions for $Z(\tilde{F}_N,D) \to P(F,D)$.
186+
3. **Constraint**: Do not assume RH.
187+
188+
**Targeting Leaf**: `RH-C2-correct`
189+
"""
190+
candidate = _extract_json(output)
191+
assert candidate["strategy_parse_mode"] == "prose"
192+
assert candidate["target_obligation_id"] == "RH-C2-correct"
193+
assert "Zero-Exclusion Lemma" in candidate["hypothesis"]
194+
assert len(candidate["plan"]["steps"]) >= 2
195+
196+
197+
def test_strategy_parser_accepts_python_literal_candidate():
198+
candidate = _extract_json(
199+
"```python\n{'candidate_id': 'trial', 'hypothesis': 'test'}\n```",
200+
)
201+
assert candidate["candidate_id"] == "trial"
202+
assert candidate["strategy_parse_mode"] == "python-literal"
203+
204+
176205
def test_keep_requires_novel_mathematical_advancement():
177206
baseline = {
178207
"proof_obligations_unresolved": "5",

0 commit comments

Comments
 (0)