Skip to content

Commit 630beb5

Browse files
fluffy314cursoragent
authored andcommitted
feat(autoresearch): make strategy replanning event-driven
Run normal proof turns from a deterministic deepest-leaf candidate and invoke Strategy Gemma only for stagnation, falsified branches, or explicit human triggers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a6c2a11 commit 630beb5

3 files changed

Lines changed: 239 additions & 20 deletions

File tree

autoresearch/prefill/program.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Use a lexicographic objective:
3232
## Experiment loop
3333

3434
1. Read `candidate.py` and `results.tsv`.
35-
2. State one concrete mathematical hypothesis for one unresolved leaf.
35+
2. Let the host deterministically select the deepest unresolved leaf.
3636
3. Modify only `candidate.py`.
3737
4. Verify Primary and allens health without restarting either service.
3838
5. Preserve all KV caches across decomposition iterations.
@@ -45,6 +45,10 @@ Use a lexicographic objective:
4545

4646
Every candidate must target exactly one current unresolved leaf obligation and
4747
contain a falsifiable hypothesis plus distinct Generator and Critic directives.
48+
Normal iterations use the deterministic host candidate and do not call the
49+
Strategy model. Invoke Strategy Gemma only after three consecutive
50+
non-progressing runs, after a falsified branch, or via an explicit CLI/trigger
51+
file request.
4852
An unresolved Critic verdict must isolate one strictly smaller missing lemma;
4953
the host records that lemma as a deduplicated child obligation. Completed GAN
5054
runs, transcripts, checkpoints, and ledger updates remain durable even when the
@@ -58,7 +62,7 @@ must be deployed outside this supervisor. Cold benchmarks are explicit,
5862
separate invocations of `scripts/benchmark_prefill_architecture.py`.
5963

6064
Prefill budgets are hard admission limits, never truncation instructions.
61-
Strategy input must fit 8192 tokens by carrying the complete active leaf
65+
Strategy input must fit 8448 tokens by carrying the complete active leaf
6266
ancestry and its exact experiment records. Generator and Critic inputs must fit
6367
6144 tokens; the Critic always receives the complete current Generator output.
6468
Repeated Strategy strings are interned once in `text_by_id`; `_ref` fields
@@ -73,6 +77,9 @@ model-invented ID. Reject a proposed child when it duplicates an existing
7377
statement or lemma name, is highly similar to an ancestor, or is too vague to
7478
be falsifiable. A rejected cyclic frontier is `INCONCLUSIVE`, not progress.
7579

80+
`DECOMPOSED` is keepable only when the host actually persisted at least one
81+
new child that passed the ID, novelty, cycle, and falsifiability gates.
82+
7683
Do not optimize output wording, scores, prizes, or other proof-irrelevant
7784
content. Prefill performance is a tertiary objective after mathematical
7885
decomposition progress, while preserving the complete semantic contract.

autoresearch/prefill/supervisor.py

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ def parse_research_verdict(output: str, candidate_id: str) -> dict:
367367
"outcome": fields["outcome"],
368368
"evidence": fields["evidence"],
369369
"new_frontier": fields["new_frontier"],
370+
"created_obligation_ids": list(
371+
fields.get("created_obligation_ids", []),
372+
),
370373
}
371374
matches = list(re.finditer(
372375
r"^(?:critic>\s*)?### AUTORESEARCH_VERDICT\s*$"
@@ -397,6 +400,7 @@ def parse_research_verdict(output: str, candidate_id: str) -> dict:
397400
"outcome": fields["Outcome"],
398401
"evidence": fields["Evidence"],
399402
"new_frontier": fields["New frontier"],
403+
"created_obligation_ids": [],
400404
}
401405

402406

@@ -626,7 +630,7 @@ def propose_candidate(
626630
current: dict,
627631
results_text: str,
628632
ledger: dict,
629-
max_prefill_tokens: int = 8192,
633+
max_prefill_tokens: int = 8448,
630634
) -> dict:
631635
from kakeya import Client
632636
from transformers import AutoTokenizer
@@ -826,14 +830,102 @@ def best_kept(results: list[dict]) -> dict | None:
826830
)
827831

828832

833+
def _created_ids(row: dict) -> list[str]:
834+
raw = row.get("created_obligation_ids", "")
835+
if isinstance(raw, list):
836+
return [str(item) for item in raw if item]
837+
if not raw:
838+
return []
839+
try:
840+
value = json.loads(raw)
841+
except (TypeError, json.JSONDecodeError):
842+
return []
843+
return [str(item) for item in value if item] if isinstance(value, list) else []
844+
845+
846+
def _row_made_progress(row: dict) -> bool:
847+
if row.get("kept") not in {True, "True"}:
848+
return False
849+
outcome = row.get("research_outcome")
850+
if outcome in {"SUPPORTED", "FALSIFIED"}:
851+
return True
852+
if outcome == "DECOMPOSED":
853+
# Legacy rows predate created_obligation_ids but were already admitted
854+
# by the host's child-creation gate.
855+
return bool(_created_ids(row)) or not row.get(
856+
"created_obligation_ids",
857+
)
858+
return False
859+
860+
861+
def strategy_trigger_reason(
862+
results: list[dict],
863+
*,
864+
stagnation_rounds: int,
865+
force: bool = False,
866+
trigger_file: Path | None = None,
867+
) -> str:
868+
if force:
869+
return "manual-cli"
870+
if trigger_file is not None and trigger_file.exists():
871+
return "manual-trigger-file"
872+
if results and results[-1].get("research_outcome") == "FALSIFIED":
873+
return "branch-falsified"
874+
stagnant = 0
875+
for row in reversed(results):
876+
if _row_made_progress(row):
877+
break
878+
stagnant += 1
879+
if stagnant >= stagnation_rounds:
880+
return f"stagnation-{stagnant}"
881+
return ""
882+
883+
884+
def build_host_candidate(current: dict, ledger: dict) -> dict:
885+
target_id = _select_repair_target(current, ledger)
886+
target = next(
887+
item
888+
for item in ledger.get("obligations", [])
889+
if item.get("obligation_id") == target_id
890+
)
891+
statement = str(target.get("statement", "")).strip()
892+
evidence = str(target.get("last_evidence", "")).strip()
893+
digest = hashlib.sha256(target_id.encode()).hexdigest()[:12]
894+
candidate = {
895+
"candidate_id": f"host-leaf-{digest}",
896+
"target_obligation_id": target_id,
897+
"hypothesis": statement,
898+
"generator_directive": (
899+
f"Resolve or falsify the exact target leaf {target_id}: "
900+
f"{statement} Previous Critic evidence: {evidence or '(none)'}. "
901+
"Provide an explicit derivation or counterexample; do not rename "
902+
"the same gap as a new lemma."
903+
),
904+
"critic_directive": (
905+
f"Adversarially test target leaf {target_id}. Reject unsupported "
906+
"existence claims and semantic restatements. Mark PROVED or "
907+
"DISPROVED only with explicit evidence; otherwise identify one "
908+
"strictly smaller, falsifiable missing obligation."
909+
),
910+
"prefill_compute_chunk_tokens": int(
911+
current["prefill_compute_chunk_tokens"],
912+
),
913+
"snapshot_mode": "final_only",
914+
"max_segment_seconds": 300.0,
915+
"require_full_context": True,
916+
"allow_fallback": False,
917+
}
918+
validate_candidate(candidate)
919+
return candidate
920+
921+
829922
def should_keep(result: dict, baseline: dict | None) -> bool:
830923
if not result["accepted"]:
831924
return False
832-
if result.get("research_outcome") not in {
833-
"SUPPORTED", "FALSIFIED", "DECOMPOSED",
834-
}:
925+
outcome = result.get("research_outcome")
926+
if outcome not in {"SUPPORTED", "FALSIFIED", "DECOMPOSED"}:
835927
return False
836-
if not result.get("hypothesis_novel", False):
928+
if outcome == "DECOMPOSED" and not result.get("created_obligation_ids"):
837929
return False
838930
if baseline is None:
839931
return True
@@ -850,7 +942,8 @@ def should_keep(result: dict, baseline: dict | None) -> bool:
850942
"proof_obligations_unresolved", "compute_chunk_tokens",
851943
"candidate_sha256", "report_path",
852944
"hypothesis_sha256", "research_outcome", "research_evidence",
853-
"new_frontier", "transcript_path", "error",
945+
"new_frontier", "created_obligation_ids", "strategy_mode",
946+
"transcript_path", "error",
854947
)
855948

856949

@@ -910,6 +1003,7 @@ def run_iteration(args, iteration: int) -> dict:
9101003
transcript_path = reports_dir / "not-started.log"
9111004
hypothesis_sha256 = ""
9121005
candidate_sha256 = hashlib.sha256(previous_candidate).hexdigest()
1006+
strategy_mode = "baseline"
9131007
try:
9141008
print(
9151009
f"[autoresearch] iteration={iteration} "
@@ -926,17 +1020,26 @@ def run_iteration(args, iteration: int) -> dict:
9261020
f"kv_hit_rate={health.get('kv_hit_rate', 0):.1%}",
9271021
flush=True,
9281022
)
1023+
ledger_data = json.loads(ledger_path.read_text())
1024+
trigger_file = Path(args.strategy_trigger_file).expanduser()
1025+
trigger_reason = strategy_trigger_reason(
1026+
results,
1027+
stagnation_rounds=args.strategy_stagnation_rounds,
1028+
force=args.force_strategy and iteration == 0,
1029+
trigger_file=trigger_file,
1030+
)
9291031
if baseline is None and iteration == 0:
9301032
print(
9311033
"[autoresearch] phase=baseline using current candidate",
9321034
flush=True,
9331035
)
934-
else:
1036+
elif trigger_reason:
1037+
strategy_mode = "gemma"
9351038
print(
936-
"[autoresearch] phase=strategy-proposal real-gemma",
1039+
"[autoresearch] phase=strategy-proposal "
1040+
f"mode=gemma trigger={trigger_reason}",
9371041
flush=True,
9381042
)
939-
ledger_data = json.loads(ledger_path.read_text())
9401043
proposed = propose_candidate(
9411044
address=args.address,
9421045
tokenizer_id=args.tokenizer_id,
@@ -954,13 +1057,24 @@ def run_iteration(args, iteration: int) -> dict:
9541057
raise ValueError(
9551058
"strategy agent targeted a non-leaf proof obligation",
9561059
)
957-
candidate_path.write_text(render_candidate(proposed))
1060+
if trigger_reason == "manual-trigger-file":
1061+
trigger_file.unlink(missing_ok=True)
1062+
else:
1063+
strategy_mode = "host"
1064+
proposed = build_host_candidate(current, ledger_data)
9581065
print(
959-
f"[autoresearch] phase=candidate-written "
960-
f"candidate={proposed['candidate_id']} "
1066+
"[autoresearch] phase=deterministic-candidate "
9611067
f"target={proposed['target_obligation_id']}",
9621068
flush=True,
9631069
)
1070+
candidate_path.write_text(render_candidate(proposed))
1071+
print(
1072+
f"[autoresearch] phase=candidate-written "
1073+
f"candidate={proposed['candidate_id']} "
1074+
f"target={proposed['target_obligation_id']} "
1075+
f"mode={strategy_mode}",
1076+
flush=True,
1077+
)
9641078
validate_candidate(proposed)
9651079
hypothesis_sha256 = hashlib.sha256(
9661080
proposed["hypothesis"].strip().lower().encode(),
@@ -970,7 +1084,8 @@ def run_iteration(args, iteration: int) -> dict:
9701084
for row in results
9711085
if row.get("hypothesis_sha256")
9721086
}
973-
if hypothesis_sha256 in seen_hypotheses:
1087+
hypothesis_novel = hypothesis_sha256 not in seen_hypotheses
1088+
if strategy_mode == "gemma" and not hypothesis_novel:
9741089
raise ValueError("strategy agent repeated a previous hypothesis")
9751090
candidate_sha256 = hashlib.sha256(
9761091
candidate_path.read_bytes(),
@@ -1011,8 +1126,9 @@ def run_iteration(args, iteration: int) -> dict:
10111126
"research_outcome": verdict["outcome"],
10121127
"research_evidence": verdict["evidence"],
10131128
"new_frontier": verdict["new_frontier"],
1129+
"created_obligation_ids": verdict["created_obligation_ids"],
10141130
"transcript_path": str(transcript_path),
1015-
"hypothesis_novel": True,
1131+
"hypothesis_novel": hypothesis_novel,
10161132
})
10171133
keep = should_keep(result, baseline)
10181134
print(
@@ -1050,6 +1166,10 @@ def run_iteration(args, iteration: int) -> dict:
10501166
"research_outcome": verdict["outcome"],
10511167
"research_evidence": verdict["evidence"],
10521168
"new_frontier": verdict["new_frontier"],
1169+
"created_obligation_ids": json.dumps(
1170+
verdict["created_obligation_ids"],
1171+
),
1172+
"strategy_mode": strategy_mode,
10531173
"transcript_path": str(transcript_path),
10541174
}
10551175
append_result(results_path, row)
@@ -1094,6 +1214,7 @@ def run_iteration(args, iteration: int) -> dict:
10941214
"report_path": str(report_path),
10951215
"hypothesis_sha256": hypothesis_sha256,
10961216
"research_outcome": "EVALUATION_FAILED",
1217+
"strategy_mode": strategy_mode,
10971218
"transcript_path": str(transcript_path),
10981219
"error": f"{type(exc).__name__}: {exc}",
10991220
}
@@ -1117,7 +1238,20 @@ def main() -> int:
11171238
parser.add_argument(
11181239
"--strategy-max-prefill-tokens",
11191240
type=int,
1120-
default=8192,
1241+
default=8448,
1242+
)
1243+
parser.add_argument(
1244+
"--strategy-stagnation-rounds",
1245+
type=int,
1246+
default=3,
1247+
)
1248+
parser.add_argument("--force-strategy", action="store_true")
1249+
parser.add_argument(
1250+
"--strategy-trigger-file",
1251+
default=str(
1252+
Path.home()
1253+
/ ".kakeya/autoresearch/request_strategy"
1254+
),
11211255
)
11221256
parser.add_argument(
11231257
"--tokenizer-id",
@@ -1148,6 +1282,8 @@ def main() -> int:
11481282
raise SystemExit("iterations must be > 0")
11491283
if args.strategy_max_prefill_tokens <= 0:
11501284
raise SystemExit("strategy-max-prefill-tokens must be > 0")
1285+
if args.strategy_stagnation_rounds <= 0:
1286+
raise SystemExit("strategy-stagnation-rounds must be > 0")
11511287
for iteration in range(args.iterations):
11521288
row = run_iteration(args, iteration)
11531289
print(json.dumps(row, indent=2, sort_keys=True))

0 commit comments

Comments
 (0)