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
+2 -2
View File
@@ -57,7 +57,7 @@ def rank_and_select(
# Build the edit pool description for the optimizer
edits_desc = []
for i, edit in enumerate(edits):
edits_desc.append(f"[{i}] {describe_item(edit, update_mode, max_chars=500)}")
edits_desc.append(f"[{i}] {describe_item(edit, update_mode)}")
user = (
f"## Current Skill\n{skill_content}\n\n"
@@ -74,7 +74,7 @@ def rank_and_select(
try:
response, _ = chat_optimizer(
system=load_prompt(prompt_name), user=user,
max_completion_tokens=2048, retries=3, stage="ranking",
max_completion_tokens=16384, retries=3, stage="ranking",
)
result = extract_json(response)
if result and "selected_indices" in result:
+2 -2
View File
@@ -48,7 +48,7 @@ def decide_autonomous_learning_rate(
items = get_payload_items(merged_patch, update_mode)
available = len(items)
item_lines = [
f"[{idx}] {describe_item(item, update_mode, max_chars=700)}"
f"[{idx}] {describe_item(item, update_mode)}"
for idx, item in enumerate(items)
]
user = (
@@ -76,7 +76,7 @@ def decide_autonomous_learning_rate(
response, _ = chat_optimizer(
system=load_prompt("lr_autonomous"),
user=user,
max_completion_tokens=2048,
max_completion_tokens=16384,
retries=3,
stage="lr_autonomous",
)
+28 -28
View File
@@ -91,18 +91,21 @@ def replace_slow_update_field(skill: str, new_content: str) -> str:
# ── Comparison text builder ─────────────────────────────────────────────────
# NOTE: The character limits below (whole-trajectory cap + the per-field caps in
# _read_trajectory and the comparison metadata) only trim the comparison samples
# fed to the slow-update optimizer. They exist to cut token usage and speed up the
# call; they do NOT affect what gets written into the skill. If you need richer
# context for the longitudinal comparison, feel free to raise them.
_MAX_TRAJ_CHARS = 3000
# NOTE: Character-length limits on the comparison samples fed to the slow-update /
# meta-skill optimizer have been REMOVED. Previously a whole-trajectory cap plus
# per-field caps (cmd/obs/reasoning/etc.) and comparison-metadata caps
# (task/answer/fail_reason) trimmed this context to save optimizer tokens and
# speed up the call. They never affected what gets written into the skill — only
# how much longitudinal context the optimizer sees. We now pass everything through
# at full length: the comparison input is as long as the source data is.
def _clip_text(value, limit: int) -> str:
def _clip_text(value, limit: int | None = None) -> str:
# Truncation disabled: return the full text. The `limit` argument is kept only
# for call-site compatibility and is intentionally ignored (see NOTE above).
if value is None:
return ""
return str(value)[:limit]
return str(value)
def _read_trajectory(rollout_dir: str, task_id: str) -> str:
@@ -122,35 +125,32 @@ def _read_trajectory(rollout_dir: str, task_id: str) -> str:
for entry in conversation:
if not isinstance(entry, dict):
continue
# Per-field caps (cmd/obs/reasoning/etc.) keep each trajectory compact to
# save tokens / time; raise them if you want fuller step detail.
# Per-field truncation removed: feed each step's full cmd/obs/reasoning/
# action/feedback/content (see NOTE above).
if entry.get("type") == "tool_call":
cmd = _clip_text(entry.get("cmd"), 500)
obs = _clip_text(entry.get("obs"), 800)
cmd = _clip_text(entry.get("cmd"))
obs = _clip_text(entry.get("obs"))
lines.append(f"[action] {cmd}")
lines.append(f"[obs] {obs}")
elif "action" in entry and "env_feedback" in entry:
step = entry.get("step", "?")
reasoning = _clip_text(entry.get("reasoning"), 300)
action = _clip_text(entry.get("action"), 200)
feedback = _clip_text(entry.get("env_feedback"), 500)
reasoning = _clip_text(entry.get("reasoning"))
action = _clip_text(entry.get("action"))
feedback = _clip_text(entry.get("env_feedback"))
if reasoning:
lines.append(f"[step {step} think] {reasoning}")
lines.append(f"[step {step} action] {action}")
lines.append(f"[step {step} obs] {feedback}")
elif entry.get("role") == "system":
msg = _clip_text(entry.get("content"), 1000)
msg = _clip_text(entry.get("content"))
lines.append(f"[verification] {msg}")
else:
msg = _clip_text(entry.get("content"), 500)
msg = _clip_text(entry.get("content"))
role = entry.get("role", "agent")
lines.append(f"[{role}] {msg}")
text = "\n".join(lines)
if len(text) > _MAX_TRAJ_CHARS:
half = _MAX_TRAJ_CHARS // 2
text = text[:half] + "\n...[truncated]...\n" + text[-half:]
return text
# Whole-trajectory truncation removed: return the full formatted trajectory.
return "\n".join(lines)
# ── Structured comparison pairs ─────────────────────────────────────────────
@@ -228,7 +228,7 @@ def save_comparison_pairs(pairs: list[dict], out_path: str) -> None:
for p in pairs:
slim.append({
"id": p["id"],
"task": p["task"][:300],
"task": p["task"],
"category": p["category"],
"prev": p["prev"],
"curr": p["curr"],
@@ -276,16 +276,16 @@ def format_comparison_text(pairs: list[dict]) -> str:
prev = e["prev"]
curr = e["curr"]
lines.append(
f"\n#### Task {e['id']}: {e['task'][:300]}\n"
f"\n#### Task {e['id']}: {e['task']}\n"
f"- Prev epoch: {'PASS' if prev['hard'] else 'FAIL'} "
f"(soft={prev['soft']:.2f}) — answer: {str(prev['predicted_answer'])[:200]}\n"
f"(soft={prev['soft']:.2f}) — answer: {str(prev['predicted_answer'])}\n"
f"- Curr epoch: {'PASS' if curr['hard'] else 'FAIL'} "
f"(soft={curr['soft']:.2f}) — answer: {str(curr['predicted_answer'])[:200]}"
f"(soft={curr['soft']:.2f}) — answer: {str(curr['predicted_answer'])}"
)
if curr.get("fail_reason"):
lines.append(f"- Curr fail reason: {curr['fail_reason'][:300]}")
lines.append(f"- Curr fail reason: {curr['fail_reason']}")
if prev.get("fail_reason") and not prev["hard"]:
lines.append(f"- Prev fail reason: {prev['fail_reason'][:300]}")
lines.append(f"- Prev fail reason: {prev['fail_reason']}")
if show_traj:
if e.get("prev_trajectory"):
+10 -11
View File
@@ -70,7 +70,7 @@ def truncate_payload(container: dict, max_items: int, mode: str | None) -> dict:
return container
def describe_item(item: dict, mode: str | None, *, max_chars: int = 240) -> str:
def describe_item(item: dict, mode: str | None, *, max_chars: int | None = None) -> str:
if not isinstance(item, dict):
return ""
if is_full_rewrite_minibatch_mode(mode):
@@ -84,7 +84,7 @@ def describe_item(item: dict, mode: str | None, *, max_chars: int = 240) -> str:
parts.append(f"support={item.get('support_count')}")
new_skill = str(item.get("new_skill", "")).strip()
if new_skill:
parts.append(f"new_skill_preview={new_skill[:120]!r}")
parts.append(f"new_skill_preview={new_skill!r}")
text = " ".join(parts)
elif is_rewrite_mode(mode):
parts = [
@@ -109,28 +109,27 @@ def describe_item(item: dict, mode: str | None, *, max_chars: int = 240) -> str:
if item.get("support_count") is not None:
parts.append(f"support={item.get('support_count')}")
text = " ".join(parts)
if len(text) <= max_chars:
return text
return text[: max_chars - 3].rstrip() + "..."
# Truncation disabled: the optimizer is given the full item description.
return text
def short_item_summary(item: dict, mode: str | None, *, max_chars: int = 200) -> dict[str, Any]:
def short_item_summary(item: dict, mode: str | None, *, max_chars: int | None = None) -> dict[str, Any]:
if is_full_rewrite_minibatch_mode(mode):
return {
"title": str(item.get("title", ""))[:max_chars],
"title": str(item.get("title", "")),
"change_summary": [
str(x)[:max_chars] for x in item.get("change_summary", [])[:3]
str(x) for x in item.get("change_summary", [])
] if isinstance(item.get("change_summary"), list) else [],
"source_type": item.get("source_type", ""),
}
if is_rewrite_mode(mode):
return {
"type": item.get("type", "?"),
"title": str(item.get("title", ""))[:max_chars],
"instruction": str(item.get("instruction", ""))[:max_chars],
"title": str(item.get("title", "")),
"instruction": str(item.get("instruction", "")),
}
return {
"op": item.get("op", "?"),
"content": str(item.get("content", ""))[:max_chars],
"content": str(item.get("content", "")),
"target": item.get("target", ""),
}