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

+44 -7
View File
@@ -33,7 +33,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from cloud.harness2 import read_tree # noqa: E402
from cloud.harness2_tasks import ( # noqa: E402
HttpTaskClient, run_improvement_via_tasks,
HttpTaskClient, preflight_checks, run_improvement_via_tasks,
validate_run_tag,
)
@@ -60,6 +61,10 @@ def main():
ap.add_argument("--mode", default="parallel", choices=["parallel", "sequential"])
ap.add_argument("--k", type=int, default=1)
ap.add_argument("--budget", type=float, default=None)
ap.add_argument("--max-sessions", type=int, default=None,
help="会话数上限(探针 + 阶段尝试;unpriced 模型上美元门不触发时的兜底)")
ap.add_argument("--check", action="store_true",
help="只做建任务前预检(本地目录/提示/tag + 控制面 health 探针),不建任务")
ap.add_argument("--run-tag", default="")
ap.add_argument("--output", default="")
ap.add_argument("--library-root", default="")
@@ -73,6 +78,34 @@ def main():
ap.add_argument("--yes", action="store_true", help="确认创建真实任务并消费配额")
args = ap.parse_args()
if args.check:
prompt = args.prompt
if args.prompt_file:
try:
prompt = Path(args.prompt_file).read_text(encoding="utf-8")
except OSError as exc:
print(f"FAIL: prompt-file 读不到:{exc}", file=sys.stderr)
return 2
errors, warnings = preflight_checks(
base_harness_dir=args.base_harness_dir,
source_dir=args.source_dir or None, prompt=prompt,
run_tag=args.run_tag, base_url=args.base_url)
for e in errors:
print(f"FAIL: {e}", file=sys.stderr)
for w in warnings:
print(f"WARN: {w}")
if not os.environ.get("CODEX_API_KEY"):
print("WARN: 未设置 CODEX_API_KEY(建任务时需要)")
if errors:
return 2
print("preflight: 通过" + ("(有告警)" if warnings else ""))
return 0
try:
validate_run_tag(args.run_tag)
except ValueError as exc:
print(f"run-tag 非法:{exc}", file=sys.stderr)
return 2
api_key = os.environ.get("CODEX_API_KEY")
if not api_key:
print("缺少 CODEX_API_KEY 环境变量(密钥不接受命令行参数)", file=sys.stderr)
@@ -90,15 +123,18 @@ def main():
return 2
run_tag = args.run_tag or time.strftime("h2-%Y%m%d-%H%M%S")
# 任务数(k=1 parallel):1 base + 2 候选 + 1 终版探针 + 3 编辑
# + 1 选择 + 1 反思 = 9;sequential 同为 9;k 越大按 3k+? 线性涨,
# 上限 k=3 时 parallel 为 1 + 6 + 3 探针 + 9 编辑 + 1 选择 + 1 反思 = 21。
# + 1 反思 = 8(k=1 时选择 trivial,不建选择任务);sequential
# 同为 8;k 越大线性涨,上限 k=3 时 parallel 为 1 + 6 + 3 探针
# + 9 编辑 + 1 选择 + 1 反思 = 21。
n_probes = 1 + 3 * args.k # 两种模式同款:1 base + 每轮 2 候选 + 1 终版
n_edits = 3 * args.k
n_select = 0 if args.k == 1 else 1 # k=1 trivial,不建选择任务
plan = (f"计划:项目 {args.project_id},任务 {args.task_id},"
f"mode={args.mode} k={args.k},run_tag={run_tag},"
f"约 {n_probes} 个探针 + {n_edits} 个编辑 + 1 选择 + 1 反思 = "
f"{n_probes + n_edits + 2} 个真实任务,"
f"budget_usd={args.budget},base 文件 {len(base)} 个。")
f"约 {n_probes} 个探针 + {n_edits} 个编辑 + {n_select} 选择 + 1 反思 = "
f"{n_probes + n_edits + n_select + 1} 个真实任务,"
f"budget_usd={args.budget},max_sessions={args.max_sessions},"
f"base 文件 {len(base)} 个。")
print(plan)
if not args.yes:
print("未带 --yes:只打印计划,不创建任何任务。确认无误后加 --yes 重跑。")
@@ -111,7 +147,8 @@ def main():
source_files=_flatten(Path(args.source_dir)) if args.source_dir else None,
k=args.k, mode=args.mode,
library_root=Path(args.library_root) if args.library_root else None,
budget_usd=args.budget, model=args.model, effort=args.effort,
budget_usd=args.budget, max_sessions=args.max_sessions,
model=args.model, effort=args.effort,
approval=args.approval, environment_id=args.environment_id,
executor=args.executor, no_time_limit=args.no_time_limit,
timeout_seconds=args.timeout)