fix(spreadsheetbench)+optimizer: fix verify-feedback bloat, drop optimizer-side truncation, soft-disable gate

A. SpreadsheetBench verification-feedback bloat
   - rollout.py _auto_verify_output: use official _compare_cell_value (was
     repr() equality, which falsely flagged 5 vs 5.0 / None vs ""); collapse
     correct-and-empty cells into a count so large sparse answer ranges no
     longer flood feedback with MBs of None=None noise.
   - codegen_agent.py _build_eval_feedback: only list WRONG cells, collapse
     correct ones into a count.
   Scoring is unaffected (evaluate() is independent); this only fixes the
   target model's multi-turn solving feedback.

B. Remove optimizer-side truncation (bloat source now fixed)
   - reflect.py: drop _MAX_TRAJ_CHARS cap and all per-field clips.
   - update_modes.py / clip.py / lr_autonomous.py: describe_item /
     short_item_summary no longer truncate; raise ranking/lr token budget.
   - trainer.py _format_step_buffer: full task_ids / target.
   - slow_update.py: full comparison samples.

C. Soft-disable gate
   - config.py / trainer.py: use_gate=false no longer raises; validation still
     runs but candidates are force-accepted (new force_accept branch + log).

Misc: aggregate.py merge token budget 4096 -> 16384.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Cuzyoung
2026-06-01 11:23:08 +00:00
parent f64a41397c
commit 372fd56c1e
10 changed files with 138 additions and 94 deletions
+40 -11
View File
@@ -24,7 +24,7 @@ from collections import defaultdict
from skillopt.datasets.base import BatchSpec
from skillopt.envs.base import EnvAdapter
from skillopt.evaluation.gate import evaluate_gate, select_gate_score
from skillopt.evaluation.gate import GateResult, evaluate_gate, select_gate_score
from skillopt.gradient.aggregate import merge_patches
from skillopt.optimizer.meta_skill import run_meta_skill
from skillopt.optimizer.clip import rank_and_select
@@ -467,7 +467,7 @@ def _format_step_buffer(buffer: list[dict]) -> str:
# Failure patterns
for p in entry.get("failure_patterns", []):
ids = ", ".join(p["task_ids"][:3])
ids = ", ".join(p["task_ids"])
parts.append(f' - "{p["pattern"]}" (×{p["count"]}, tasks: {ids})')
# Rejected edits (only present on reject)
@@ -484,7 +484,7 @@ def _format_step_buffer(buffer: list[dict]) -> str:
content = e.get("content", "")
target = e.get("target", "")
if target:
parts.append(f' {i}. [{op}] target="{target[:80]}""{content}"')
parts.append(f' {i}. [{op}] target="{target}""{content}"')
else:
parts.append(f' {i}. [{op}] "{content}"')
else:
@@ -863,11 +863,10 @@ class ReflACTTrainer:
sel_cache[sh] = (rec["selection_hard"], rec["selection_soft"])
# ── Baseline evaluation on selection set ─────────────────────────
if cfg.get("use_gate") is False:
raise ValueError(
"Gate validation is mandatory in this branch. Remove "
"`evaluation.use_gate=false` from the config."
)
# `use_gate=False` keeps validation running (selection rollout +
# scoring are unconditional below) but force-accepts every candidate
# instead of gating it; final skill is chosen manually afterwards.
use_gate = cfg.get("use_gate", True) is not False
gate_metric = str(cfg.get("gate_metric", "hard")).strip().lower()
if gate_metric not in {"hard", "soft", "mixed"}:
raise ValueError(
@@ -887,6 +886,8 @@ class ReflACTTrainer:
if gate_metric == "mixed"
else ""
)
+ ("" if use_gate
else " (DISABLED → validation runs, candidates force-accepted)")
)
slow_gate_with_selection = bool(
cfg.get("slow_update_gate_with_selection", False)
@@ -1346,10 +1347,31 @@ class ReflACTTrainer:
cand_soft=cand_soft,
metric=gate_metric,
mixed_weight=gate_mixed_weight,
)
) if use_gate else None
cand_gate_score = select_gate_score(
cand_hard, cand_soft, gate_metric, gate_mixed_weight,
)
if not use_gate:
# Validation ran (scores recorded above) but the gate is
# disabled: force-accept the candidate as the new current
# skill. Best-so-far is still tracked for convenience; the
# final skill is selected manually from the trajectory.
if cand_gate_score > best_score:
fa_best_skill = candidate_skill
fa_best_score = cand_gate_score
fa_best_step = global_step
else:
fa_best_skill = best_skill
fa_best_score = best_score
fa_best_step = best_step
gate = GateResult(
action="force_accept",
current_skill=candidate_skill,
current_score=cand_gate_score,
best_skill=fa_best_skill,
best_score=fa_best_score,
best_step=fa_best_step,
)
step_rec["gate_metric"] = gate_metric
step_rec["candidate_gate_score"] = cand_gate_score
step_rec["action"] = gate.action
@@ -1360,9 +1382,11 @@ class ReflACTTrainer:
best_skill = gate.best_skill
best_score = gate.best_score
best_step = gate.best_step
if gate.action in {"accept", "accept_new_best"}:
if gate.action in {"accept", "accept_new_best", "force_accept"}:
current_origin = f"step_{global_step:04d}"
if gate.action == "accept_new_best":
if gate.action == "accept_new_best" or (
gate.action == "force_accept" and best_step == global_step
):
best_origin = current_origin
if gate_metric == "hard":
@@ -1384,6 +1408,11 @@ class ReflACTTrainer:
f" [6/6 EVALUATE] ACCEPT "
f"{score_label} > current={prev_current:.4f}"
)
elif gate.action == "force_accept":
print(
f" [6/6 EVALUATE] FORCE-ACCEPT (gate disabled) "
f"{score_label}"
)
else:
print(
f" [6/6 EVALUATE] REJECT "