Arabi32 commited on
Commit
4f2cc8b
·
verified ·
1 Parent(s): 344d05e

Upload app_kimi.py

Browse files
Files changed (1) hide show
  1. app_kimi.py +203 -17
app_kimi.py CHANGED
@@ -10,6 +10,18 @@ SCOPE: this collects data only. It does NOT train anything -- no DPO loop,
10
  no optimizer, nothing that touches gradients. Output is training-ready JSONL
11
  for someone else's training scripts to consume.
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  WHAT CHANGED IN v10 (code-enforced general agent-thinker trajectories, single-file-per-worker storage,
14
  anonymization, Dataset Viewer fix)
15
  ----------------------------------------------------------------------------------------------------------------
@@ -2041,6 +2053,161 @@ class LlamaCppClient:
2041
  return ok
2042
 
2043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2044
  def start_llama_backend():
2045
  """-> (client, server). Model download, binary build/download and tokenizer load run in parallel."""
2046
  from concurrent.futures import ThreadPoolExecutor
@@ -2062,7 +2229,7 @@ def start_llama_backend():
2062
  if kind == "gpu" and not re.search(r"offloaded|CUDA\d", server.tail(600)):
2063
  print("[llama][WARNING] the server log does not mention CUDA/offloading -- it may be running on CPU. "
2064
  f"Check {server.log_path}", flush=True)
2065
- client = LlamaCppClient(server, tok, kind)
2066
  print(f"[llama] context per request: {client.ctx} tokens | server adds BOS: {client.server_adds_bos}", flush=True)
2067
  if not client.warmup():
2068
  print("[llama][WARNING] warmup produced no text; continuing, but check llama-server.log", flush=True)
@@ -2100,18 +2267,20 @@ def _strip_atem_noise(text: str) -> str:
2100
  return _channel_noise_re().sub(" ", text or "").strip()
2101
 
2102
 
2103
- _THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
2104
- _THINK_DANGLING_RE = re.compile(r"<think>.*\Z", re.DOTALL) # closing tag never arrived (token cap / timeout)
 
 
 
2105
 
2106
 
2107
  def _strip_reasoning_leak(text: str) -> str:
2108
- """v8: this model's OWN chat template (not this harness) defines a native <think>...</think> convention --
2109
- see WHAT CHANGED IN v8 for exactly which models do and don't, confirmed per-model via each one's own
2110
- chat_template.jinja/tokenizer_config.json rather than assumed. Applied at the same call sites
2111
- _strip_atem_noise already is, so no stored field or trajectory turn keeps one model's thinking-tag residue
2112
- while another's stays clean -- a no-op for a model with no thinking mode, defense-in-depth for one whose
2113
- thinking is suppressed by _CHAT_TEMPLATE_EXTRA_KWARGS below, the only backstop for one with no off-switch."""
2114
  text = _THINK_BLOCK_RE.sub(" ", text or "")
 
2115
  return _THINK_DANGLING_RE.sub(" ", text).strip()
2116
 
2117
 
@@ -3015,7 +3184,8 @@ def _validate_fields(fields: dict, language: str, limits: dict):
3015
  if "<atem:" in val:
3016
  return f"{name} contains tool-protocol markup"
3017
  if ("<think" in val or "</think>" in val or "<tool_call" in val or "<minimax:tool_call" in val
3018
- or "\uff5cDSML\uff5c" in val):
 
3019
  return f"{name} contains a model-native reasoning/tool-call leak" # v8: backstop, see WHAT CHANGED IN v8
3020
  if len(val) < lo:
3021
  return f"{name} too short ({len(val)}<{lo} chars)"
@@ -3381,7 +3551,7 @@ _TASK_TAG_RE = re.compile(r"<task>(.*?)</task>", re.DOTALL | re.IGNORECASE)
3381
  _NEEDS_TOOLS_RE = re.compile(r"NEEDS_TOOLS\s*:\s*(yes|no)", re.IGNORECASE)
3382
  _REVISION_RE = re.compile(r"REVISION\s*:\s*(.*)", re.DOTALL | re.IGNORECASE)
3383
  _RATIONALE_RE = re.compile(r"RATIONALE:\s*(.*)")
3384
- _PY_CODE_BLOCK_RE = re.compile(r"```(?:python|py)?\s*\n(.*?)```", re.DOTALL)
3385
 
3386
  EXECUTE_CODE_TIMEOUT_S = int(os.environ.get("GIJL_EXECUTE_CODE_TIMEOUT_S", "20"))
3387
  EXECUTE_CODE_MAX_OUTPUT_CHARS = 4000
@@ -3454,6 +3624,18 @@ def _grounding_check(proposal: str, research_steps: list) -> dict:
3454
  return {"passed": passed, "overlap_ratio": round(overlap, 3), "reason": reason}
3455
 
3456
 
 
 
 
 
 
 
 
 
 
 
 
 
3457
  def _code_syntax_ok(text: str):
3458
  """Cheap, real, non-LLM pre-check (DeepMind's synthetic-data finding: verify with code execution where
3459
  you can). A syntax check only -- used as a fast path so an unparseable block never reaches the heavier
@@ -3532,9 +3714,11 @@ Your own plan: {think}
3532
 
3533
  {research_note}
3534
 
3535
- If your answer includes code, put it in a single fenced ```python code block -- it WILL be executed for real
3536
- to verify it before anything else happens, so make it runnable as-is (no placeholders, no "...", include any
3537
- needed imports), not just a sketch. If it makes factual/technical claims, make sure they are grounded in what
 
 
3538
  you found above, not guessed. Do not pad the answer with meta-commentary about your process -- just the
3539
  answer a real person would receive.
3540
 
@@ -3571,7 +3755,8 @@ RECOVERY_FIX_TEMPLATE = """Based on what you just found, write ONE consolidated
3571
  not conflict with your original approach -- reconcile the candidate fixes you found into a single coherent
3572
  answer rather than patching in every suggestion. Before the fix itself, write one line starting "RATIONALE:"
3573
  explaining briefly why this is the right fix given what you found. Then give the complete, corrected final
3574
- answer (same format rules as before: a single runnable ```python block if there is code, no placeholders).
 
3575
 
3576
  <task>{task_prompt}</task>
3577
 
@@ -3600,9 +3785,10 @@ def _run_verification(client, proposal: str, research_steps: list, task_prompt:
3600
  """Harness-selected, not model-selected: picks the verification method from what `proposal` actually
3601
  contains, runs it for real, and returns {"method", "passed", "evidence"}. This is what makes
3602
  "verify everything you produce" generalize across arbitrary task kinds instead of one fixed routine."""
3603
- code_blocks = _PY_CODE_BLOCK_RE.findall(proposal)
3604
  if code_blocks:
3605
- if _code_syntax_ok(proposal) is False:
 
3606
  return {"method": "code_execution", "passed": False,
3607
  "evidence": {"stderr": "SyntaxError: the proposed code does not even parse", "stdout": "",
3608
  "exit_code": None, "timed_out": False}}
 
10
  no optimizer, nothing that touches gradients. Output is training-ready JSONL
11
  for someone else's training scripts to consume.
12
 
13
+ WHAT CHANGED IN v10.1 (field report on the live shards: reasoning-tag leak, 5x unclosed thinking, CSS run as Python)
14
+ ----------------------------------------------------------------------------------------------------------------
15
+ * _strip_reasoning_leak() now also removes an ORPHAN close tag (a close with no open) in addition to complete
16
+ pairs and dangling opens, for both delimiter conventions (<think> and kimi's triangle-glyph tags);
17
+ _validate_fields() rejects any field still containing the glyph form. (All workers.)
18
+ * Code fences: only ```python / ```py / ```python3 blocks are executed (an untagged fence used to match, so
19
+ CSS/HTML/shell was run as Python and failed with a guaranteed SyntaxError); a block fenced as python that is
20
+ plainly HTML/CSS/shell is skipped for execution; PROPOSE and RECOVERY templates now say other languages must
21
+ use their own fence tag. (All workers.)
22
+ * kimi only: KimiThinkBudgetClient -- capped two-stage generation (GIJL_KIMI_THINK_BUDGET, default 1200), the
23
+ same cure MinimaxThinkBudgetClient applies to minimax, adapted to kimi writing its own opening tag.
24
+
25
  WHAT CHANGED IN v10 (code-enforced general agent-thinker trajectories, single-file-per-worker storage,
26
  anonymization, Dataset Viewer fix)
27
  ----------------------------------------------------------------------------------------------------------------
 
2053
  return ok
2054
 
2055
 
2056
+ _KIMI_THINK_OPEN = "\u25c1think\u25b7"
2057
+ _KIMI_THINK_CLOSE = "\u25c1/think\u25b7"
2058
+ _KIMI_THINK_BUDGET = int(os.environ.get("GIJL_KIMI_THINK_BUDGET", "1200"))
2059
+
2060
+
2061
+ def _strip_special_keep_think(tok, text: str, keep_atem: bool) -> str:
2062
+ """Like _strip_special_tokens, but never deletes a think-delimiter token: if kimi's glyph tags are
2063
+ registered as special tokens, the stock stripper would remove them and the stage logic (and the later
2064
+ stripper) could no longer see where reasoning starts and ends."""
2065
+ for t in set(getattr(tok, "all_special_tokens", []) or []):
2066
+ if not t:
2067
+ continue
2068
+ low = t.lower()
2069
+ if "think" in low or (keep_atem and "atem" in low):
2070
+ continue
2071
+ text = text.replace(t, "")
2072
+ return text
2073
+
2074
+
2075
+ class KimiThinkBudgetClient(LlamaCppClient):
2076
+ """v10.1, this worker only -- field evidence from the live shard: kimi writes its own reasoning block
2077
+ delimited by the triangle-glyph think tags (29 opens vs 6 closes in one file), i.e. in roughly four of five
2078
+ turns it never closes the block inside the token budget, so the visible answer is empty or cut mid-thought.
2079
+ Same disease as minimax's (see MinimaxThinkBudgetClient), same cure: generation is split into two
2080
+ /completion calls against the same slot. Stage 1 runs with the close tag as a stop string and a capped
2081
+ budget (GIJL_KIMI_THINK_BUDGET, default 1200 tokens). If it stops on the close tag, or hits the cap while a
2082
+ block is open, the close tag is written (forced in the second case) and stage 2 continues from there with
2083
+ the worker's full max_new_tokens for the answer / tool call; the second call reuses the first one's KV
2084
+ cache (exact prompt-prefix match). If stage 1 ends on its own (no reasoning block at all), or hits the cap
2085
+ while writing a plain long answer, nothing is forced: the first case is returned as is, the second simply
2086
+ continues. Unlike minimax, no opening tag is added back: kimi emits its own, so the combined text is
2087
+ byte-for-byte what a single uncapped call would have produced. Best-effort: raise
2088
+ GIJL_KIMI_THINK_BUDGET if the [llama][kimi] forced-close line shows up on most turns."""
2089
+
2090
+ def chat(self, messages, max_new_tokens=GEN_MAX_NEW_TOKENS, temperature=0.4, tools=None):
2091
+ import requests
2092
+ tool_mode = bool(tools)
2093
+ self.server.ensure_running()
2094
+ base_prompt = self._prepare_prompt(_render_chat(self.tok, messages, tools))
2095
+ try:
2096
+ n_prompt = len(self._tokenize(base_prompt, True))
2097
+ except requests.RequestException:
2098
+ n_prompt = -1
2099
+ if n_prompt >= 0 and n_prompt + 256 > self.ctx:
2100
+ print(f"[llama][kimi] prompt is {n_prompt} tokens with ctx={self.ctx} -- refusing to generate", flush=True)
2101
+ return {"content": "", "error": "context_too_long", "timed_out": False, "prompt_tokens": n_prompt}
2102
+
2103
+ think_budget = _KIMI_THINK_BUDGET
2104
+ if n_prompt >= 0:
2105
+ think_budget = max(256, min(think_budget, self.ctx - n_prompt - max_new_tokens - 64))
2106
+ s1 = self._raw_completion(base_prompt, think_budget, temperature, [_KIMI_THINK_CLOSE], tool_mode)
2107
+
2108
+ def _result(content, s2=None):
2109
+ s2 = s2 or {"cached": 0, "gen": 0, "error": None, "timed_out": False}
2110
+ return {"content": content, "error": s1["error"] or s2["error"],
2111
+ "timed_out": s1["timed_out"] or s2["timed_out"], "prompt_tokens": n_prompt,
2112
+ "cached_tokens": s1["cached"] + s2["cached"], "gen_tokens": s1["gen"] + s2["gen"]}
2113
+
2114
+ body = s1["content"]
2115
+ if s1["error"] or s1["timed_out"]:
2116
+ return _result(body)
2117
+ stop_type = s1.get("stop_type")
2118
+ if stop_type == "word" or _KIMI_THINK_CLOSE in body:
2119
+ # the model closed its own reasoning; the stop string itself is not part of the streamed text
2120
+ if _KIMI_THINK_CLOSE not in body:
2121
+ body = body + _KIMI_THINK_CLOSE
2122
+ body = body.rstrip() + "\n"
2123
+ elif stop_type == "limit" and _KIMI_THINK_OPEN in body:
2124
+ body = body.rstrip() + "\n" + _KIMI_THINK_CLOSE + "\n"
2125
+ print(f"[llama][kimi] reasoning hit the {think_budget}-token think-budget before closing itself "
2126
+ f"-- forced the close tag and continuing to the answer stage", flush=True)
2127
+ elif stop_type != "limit":
2128
+ return _result(body) # finished on its own (EOS / other): a complete answer, nothing to continue
2129
+ # else: cap reached while writing a plain (non-reasoning) answer -> just continue it in stage 2
2130
+
2131
+ s2 = self._raw_completion(base_prompt + body, max(64, max_new_tokens), temperature, None, tool_mode)
2132
+ return _result(body + s2["content"], s2)
2133
+
2134
+ def _raw_completion(self, prompt, n_predict, temperature, stop, tool_mode):
2135
+ """One /completion call -- same streaming loop, timeouts and SSE parsing as LlamaCppClient.chat();
2136
+ additionally reports llama-server's stop_type ('word' = a stop string matched, 'limit' = n_predict
2137
+ reached, 'eos' = the model finished)."""
2138
+ import requests
2139
+ payload = {"prompt": prompt, "n_predict": n_predict, "temperature": temperature, "top_p": TOP_P, "top_k": TOP_K,
2140
+ "stream": True, "cache_prompt": True}
2141
+ if stop:
2142
+ payload["stop"] = stop
2143
+ payload.update(self.extra_payload)
2144
+
2145
+ gap = GEN_PER_CHUNK_GAP_TIMEOUT_S.get(self.kind, 300)
2146
+ cap = GEN_MAX_TOTAL_S.get(self.kind, 900)
2147
+ chunks, final, err, timed_out, resp = [], None, None, False, None
2148
+ start = last_data = last_hb = time.time()
2149
+ try:
2150
+ resp = requests.post(self.server.base_url + "/completion", json=payload, stream=True, timeout=(15, gap))
2151
+ if resp.status_code != 200:
2152
+ body = resp.text[:400]
2153
+ err = "context_too_long" if "exceed_context_size" in body else f"HTTP {resp.status_code}: {_redact(body)}"
2154
+ else:
2155
+ for raw in resp.iter_lines(chunk_size=1):
2156
+ now = time.time()
2157
+ if now - start > cap:
2158
+ print(f"[gen][TIMEOUT][kimi] hit the {cap}s hard cap after {len(chunks)} chunks -- cancelling generation.", flush=True)
2159
+ timed_out = True
2160
+ break
2161
+ if now - last_data > gap:
2162
+ print(f"[gen][TIMEOUT][kimi] no generated text for {gap}s -- cancelling generation.", flush=True)
2163
+ timed_out = True
2164
+ break
2165
+ line = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw
2166
+ if not line.startswith("data:"):
2167
+ continue
2168
+ try:
2169
+ d = json.loads(line[5:].strip())
2170
+ except ValueError:
2171
+ continue
2172
+ if "error" in d:
2173
+ e = d["error"]
2174
+ err = _redact(str(e.get("message", e)) if isinstance(e, dict) else str(e))
2175
+ break
2176
+ last_data = now
2177
+ if d.get("content"):
2178
+ chunks.append(d["content"])
2179
+ if d.get("stop"):
2180
+ final = d
2181
+ break
2182
+ if now - last_hb > 30:
2183
+ print(f"[gen][kimi] {sum(len(c) for c in chunks)} chars, {now - start:.0f}s elapsed ({self.kind})", flush=True)
2184
+ last_hb = now
2185
+ except requests.RequestException as e:
2186
+ if isinstance(e, requests.exceptions.Timeout) or "timed out" in str(e).lower():
2187
+ print(f"[gen][TIMEOUT][kimi] llama-server sent nothing for {gap}s -- treating as stuck.", flush=True)
2188
+ timed_out = True
2189
+ else:
2190
+ err = f"{type(e).__name__}: {_redact(str(e))[:200]}"
2191
+ finally:
2192
+ if resp is not None:
2193
+ resp.close()
2194
+
2195
+ cached = gen = 0
2196
+ stop_type = None
2197
+ if final:
2198
+ t = final.get("timings") or {}
2199
+ cached, gen = int(t.get("cache_n") or 0), int(final.get("tokens_predicted") or 0)
2200
+ stop_type = final.get("stop_type")
2201
+ print(f"[llama][kimi] prompt {final.get('tokens_evaluated')} tok (cached {cached}) prefill "
2202
+ f"{t.get('prompt_per_second') or 0:.0f} tok/s | generated {gen} tok @ "
2203
+ f"{t.get('predicted_per_second') or 0:.1f} tok/s | stop={stop_type}", flush=True)
2204
+ if err and err != "context_too_long":
2205
+ print(f"[llama][kimi] request failed: {err}", flush=True)
2206
+ text = _strip_special_keep_think(self.tok, "".join(chunks), keep_atem=tool_mode)
2207
+ return {"content": text, "error": err, "timed_out": timed_out, "cached": cached, "gen": gen,
2208
+ "stop_type": stop_type}
2209
+
2210
+
2211
  def start_llama_backend():
2212
  """-> (client, server). Model download, binary build/download and tokenizer load run in parallel."""
2213
  from concurrent.futures import ThreadPoolExecutor
 
2229
  if kind == "gpu" and not re.search(r"offloaded|CUDA\d", server.tail(600)):
2230
  print("[llama][WARNING] the server log does not mention CUDA/offloading -- it may be running on CPU. "
2231
  f"Check {server.log_path}", flush=True)
2232
+ client = KimiThinkBudgetClient(server, tok, kind)
2233
  print(f"[llama] context per request: {client.ctx} tokens | server adds BOS: {client.server_adds_bos}", flush=True)
2234
  if not client.warmup():
2235
  print("[llama][WARNING] warmup produced no text; continuing, but check llama-server.log", flush=True)
 
2267
  return _channel_noise_re().sub(" ", text or "").strip()
2268
 
2269
 
2270
+ _THINK_OPEN_RE = r"(?:<think>|\u25c1think\u25b7)"
2271
+ _THINK_CLOSE_RE = r"(?:</think>|\u25c1/think\u25b7)"
2272
+ _THINK_BLOCK_RE = re.compile(_THINK_OPEN_RE + r".*?" + _THINK_CLOSE_RE, re.DOTALL)
2273
+ _THINK_ORPHAN_CLOSE_RE = re.compile(r"\A.*" + _THINK_CLOSE_RE, re.DOTALL) # a close with no open: everything before it was reasoning
2274
+ _THINK_DANGLING_RE = re.compile(_THINK_OPEN_RE + r".*\Z", re.DOTALL) # closing tag never arrived (token cap / timeout)
2275
 
2276
 
2277
  def _strip_reasoning_leak(text: str) -> str:
2278
+ """v8/v10.1: strips a model's own native reasoning block in whichever delimiter convention its chat
2279
+ template uses (ASCII think tags, or kimi's triangle-glyph variant -- confirmed from live output, not from
2280
+ a model card). Order matters: complete pairs first, then an orphan close (reasoning whose opening tag was
2281
+ baked into the prompt or cut off), then a dangling open (reasoning that never closed)."""
 
 
2282
  text = _THINK_BLOCK_RE.sub(" ", text or "")
2283
+ text = _THINK_ORPHAN_CLOSE_RE.sub(" ", text)
2284
  return _THINK_DANGLING_RE.sub(" ", text).strip()
2285
 
2286
 
 
3184
  if "<atem:" in val:
3185
  return f"{name} contains tool-protocol markup"
3186
  if ("<think" in val or "</think>" in val or "<tool_call" in val or "<minimax:tool_call" in val
3187
+ or "\uff5cDSML\uff5c" in val
3188
+ or "\u25c1think\u25b7" in val or "\u25c1/think\u25b7" in val):
3189
  return f"{name} contains a model-native reasoning/tool-call leak" # v8: backstop, see WHAT CHANGED IN v8
3190
  if len(val) < lo:
3191
  return f"{name} too short ({len(val)}<{lo} chars)"
 
3551
  _NEEDS_TOOLS_RE = re.compile(r"NEEDS_TOOLS\s*:\s*(yes|no)", re.IGNORECASE)
3552
  _REVISION_RE = re.compile(r"REVISION\s*:\s*(.*)", re.DOTALL | re.IGNORECASE)
3553
  _RATIONALE_RE = re.compile(r"RATIONALE:\s*(.*)")
3554
+ _PY_CODE_BLOCK_RE = re.compile(r"```(?:python3|python|py)[ \t]*\n(.*?)```", re.DOTALL) # v10.1: the tag is REQUIRED -- an untagged/other-language fence is never executed as Python
3555
 
3556
  EXECUTE_CODE_TIMEOUT_S = int(os.environ.get("GIJL_EXECUTE_CODE_TIMEOUT_S", "20"))
3557
  EXECUTE_CODE_MAX_OUTPUT_CHARS = 4000
 
3624
  return {"passed": passed, "overlap_ratio": round(overlap, 3), "reason": reason}
3625
 
3626
 
3627
+ _NON_PYTHON_FIRST_LINE_RE = re.compile(r"^\s*(?:<[!/?A-Za-z]|#!/|\$ |@(?:media|import|charset)\b)")
3628
+ _CSS_RULE_RE = re.compile(r"^[^\n=()]{1,120}\{\s*[\w-]+\s*:[^}]*;", re.DOTALL)
3629
+
3630
+
3631
+ def _block_is_non_python(code: str) -> bool:
3632
+ """v10.1: a block the model fenced as python but that is plainly HTML / CSS / shell. Executing it as Python
3633
+ can only fail, which then poisoned the verification result (live case: a CSS flexbox answer). Such a block
3634
+ is simply not a candidate for code execution; verification falls through to the grounding check."""
3635
+ c = (code or "").strip()
3636
+ return bool(_NON_PYTHON_FIRST_LINE_RE.match(c) or _CSS_RULE_RE.match(c))
3637
+
3638
+
3639
  def _code_syntax_ok(text: str):
3640
  """Cheap, real, non-LLM pre-check (DeepMind's synthetic-data finding: verify with code execution where
3641
  you can). A syntax check only -- used as a fast path so an unparseable block never reaches the heavier
 
3714
 
3715
  {research_note}
3716
 
3717
+ If your answer includes a complete Python program or script, put it in a single fenced ```python block --
3718
+ ONLY ```python blocks are executed for real to verify them, so make that block runnable as-is (no placeholders,
3719
+ no "...", include any needed imports), not just a sketch. Code in ANY OTHER language or format (CSS, HTML,
3720
+ JavaScript, shell, SQL, YAML/config, plain snippets) must use its own fence tag (```css, ```html, ```bash, ...)
3721
+ and must NEVER be labeled ```python -- it is not executed, it is simply part of the answer. If it makes factual/technical claims, make sure they are grounded in what
3722
  you found above, not guessed. Do not pad the answer with meta-commentary about your process -- just the
3723
  answer a real person would receive.
3724
 
 
3755
  not conflict with your original approach -- reconcile the candidate fixes you found into a single coherent
3756
  answer rather than patching in every suggestion. Before the fix itself, write one line starting "RATIONALE:"
3757
  explaining briefly why this is the right fix given what you found. Then give the complete, corrected final
3758
+ answer (same format rules as before: Python goes in one runnable ```python block, any other language in its
3759
+ own fence tag and never ```python, no placeholders).
3760
 
3761
  <task>{task_prompt}</task>
3762
 
 
3785
  """Harness-selected, not model-selected: picks the verification method from what `proposal` actually
3786
  contains, runs it for real, and returns {"method", "passed", "evidence"}. This is what makes
3787
  "verify everything you produce" generalize across arbitrary task kinds instead of one fixed routine."""
3788
+ code_blocks = [b for b in _PY_CODE_BLOCK_RE.findall(proposal) if not _block_is_non_python(b)]
3789
  if code_blocks:
3790
+ import ast as _ast
3791
+ if not all(_safe_ast_parse(_ast, b) for b in code_blocks):
3792
  return {"method": "code_execution", "passed": False,
3793
  "evidence": {"stderr": "SyntaxError: the proposed code does not even parse", "stdout": "",
3794
  "exit_code": None, "timed_out": False}}