harness2: upstream gap fill — trivial selection fix, session budget, run_tag validation, preflight check

- select_final trivial condition now k==1 and single-candidate pool,
  regardless of whether a selector is provided (previously gated on
  selector is None, so wired runs always paid for a useless selector
  task at k=1)
- Budget gains max_sessions gate (probe + stage-attempt sessions;
  fmt2 corrections and selector/reflector sessions exempt), the
  backstop for unpriced models where the USD gate never binds
- validate_run_tag (charset, no leading '-', no s<N> seed prefix)
  enforced at run_improvement_via_tasks and the CLI
- CLI --check preflight (base harness, source dir, prompt, tag,
  control-plane /health probe) without creating tasks
- ModeResult.to_dict now includes rollout summaries so a lost probe
  is visible in full_result instead of only on the server
- CLI plan counts the selector only when k>1
This commit is contained in:
host committed 2026-10-29 15:31:58 +08:00
1 parent 3d60f32bd3
commit 4ed1479d7a
5 files changed
+247 -26

No files matched your search

+66 -2
View File
@@ -62,6 +62,7 @@ parallel 一轮 = 1 base + 2 候选 + 1 终版探针 + 3 编辑 + 1 选择 +
from __future__ import annotations
import json
import re
import time
import urllib.error
import urllib.request
@@ -91,6 +92,67 @@ DEFAULT_TASK_TIMEOUT_SECONDS = 3600
DEFAULT_POLL_SECONDS = 10
TRACE_MAX_CHARS = 50000
_TAG_RX = re.compile(r"[A-Za-z0-9_.][A-Za-z0-9_.\-]*\Z")
_SEED_TAG_RX = re.compile(r"s\d+(-|\Z)")
def validate_run_tag(tag: str) -> None:
"""校验 run_tag 形状(上游 schedule.validate_tag 的对照补齐)。
run_tag 会进幂等键与路径段:空串表示“调用方稍后生成”,不
校验;非空时只允许字母数字与 _ . -、首字符不能是 -、不能
以 s + 数字开头(上游用 s<n> 段表示 seed,撞名会把两轮的
产物算到同一命名空间)。
"""
if not tag:
return
if not _TAG_RX.match(tag):
raise ValueError(
f"run_tag {tag!r} must match [A-Za-z0-9_.-] with no leading '-' "
"and no '/'")
if _SEED_TAG_RX.match(tag):
raise ValueError(
f"run_tag {tag!r} must not start with a seed segment like 's1'")
def preflight_checks(*, base_harness_dir: str | Path, source_dir: str | Path | None = None,
prompt: str = "", run_tag: str = "",
base_url: str | None = None) -> tuple[list[str], list[str]]:
"""建任务前的本地 + 可达性预检(2026-10-29 对照补齐)。
返回 (errors, warnings):errors 非空时调用方不得建任务;
warnings 只告警(health 探针失败、run_tag 为空待生成等不
拦任务)。本函数不读密钥、不建任务,--check 可离线跑。
"""
errors: list[str] = []
warnings: list[str] = []
base_dir = Path(base_harness_dir)
agents = base_dir / "AGENTS.md"
if not base_dir.is_dir():
errors.append(f"base-harness-dir 不存在或不是目录:{base_harness_dir}")
elif not agents.is_file() or not agents.read_text(
encoding="utf-8", errors="replace").strip():
errors.append(f"base-harness-dir 里没有可用的 AGENTS.md:{base_harness_dir}")
if source_dir is not None and str(source_dir) != "":
if not Path(source_dir).is_dir():
errors.append(f"source-dir 不存在或不是目录:{source_dir}")
if not (prompt or "").strip():
errors.append("需要 --prompt 或 --prompt-file(任务提示为空)")
if run_tag:
try:
validate_run_tag(run_tag)
except ValueError as exc:
errors.append(str(exc))
if base_url:
try:
req = urllib.request.Request(base_url.rstrip("/") + "/health")
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
warnings.append(f"控制面 health 非 200:{resp.status}")
except Exception as exc:
warnings.append(f"控制面 health 探针失败(不拦任务):{type(exc).__name__}: {exc}")
return errors, warnings
def unwrap_result_text(raw: str) -> str:
"""把服务端 result_text 的 JSON 信封拆开,只取 agent 的最终回复。
@@ -709,6 +771,7 @@ def run_improvement_via_tasks(
mode: str = "parallel",
library_root: Path | None = None,
budget_usd: float | None = None,
max_sessions: int | None = None,
model: str | None = None,
effort: str | None = None,
approval: str | None = None,
@@ -739,10 +802,11 @@ def run_improvement_via_tasks(
selector = CodexSelector(client=client, project_id=project_id, run_tag=run_tag, **shared)
reflector = CodexReflector(client=client, project_id=project_id, run_tag=run_tag, **shared)
library = Library(library_root, domain) if library_root is not None else None
validate_run_tag(run_tag)
return improve_task(
adapter, editor, selector=selector, reflector=reflector,
work_dir=work_dir, k=k, mode=mode, library=library,
auto_apply=False, budget_usd=budget_usd)
auto_apply=False, budget_usd=budget_usd, max_sessions=max_sessions)
__all__ = [
@@ -750,5 +814,5 @@ __all__ = [
"TRACE_MAX_CHARS", "TaskState", "TaskClient", "build_multipart",
"HttpTaskClient", "CompletedTask", "run_task",
"CodexProbeAdapter", "CodexEditor", "CodexSelector", "CodexReflector",
"run_improvement_via_tasks",
"run_improvement_via_tasks", "validate_run_tag", "preflight_checks",
]