From 4ed1479d7a1fbe07372d83b3aea136328f940563 Mon Sep 17 00:00:00 2001 From: host Date: Thu, 29 Oct 2026 15:31:55 +0800 Subject: [PATCH] =?UTF-8?q?harness2:=20upstream=20gap=20fill=20=E2=80=94?= =?UTF-8?q?=20trivial=20selection=20fix,=20session=20budget,=20run=5Ftag?= =?UTF-8?q?=20validation,=20preflight=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 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 --- cloud/harness2.py | 52 +++++++++++++++-- cloud/harness2_tasks.py | 68 ++++++++++++++++++++- cloud/tests/test_harness2_tasks.py | 94 +++++++++++++++++++++++++++--- docs/harness2.md | 8 ++- examples/harness2_run.py | 51 +++++++++++++--- 5 files changed, 247 insertions(+), 26 deletions(-) diff --git a/cloud/harness2.py b/cloud/harness2.py index fbc8655..d3210ba 100644 --- a/cloud/harness2.py +++ b/cloud/harness2.py @@ -26,8 +26,9 @@ Claude CLI 通道都没有搬。 选终版时只比“产物”:候选产物先匿名化(A/B/C……,按任务+k+模式 定种子洗牌,可复现),选择器看不到哪个产物出自哪个 harness;选不 -出来时回落到已交付的终版,绝不抛异常把整轮炸掉。k=1 且没有选择 -器时直接走 trivial:唯一的终版就是答案,不必再选一次。 +出来时回落到已交付的终版,绝不抛异常把整轮炸掉。k=1 且终版池 +只有唯一候选时直接走 trivial:唯一的终版就是答案,不必再选 +一次(与选择器是否提供无关)。 经验库(上游叫 harnessing-level recursion): 采纳后,反思器只写一条有证据的经验(cue / lesson / evidence 三项 @@ -740,16 +741,32 @@ def _mission_experience() -> str: @dataclass class Budget: + """美元门 + 会话数门(2026-10-29 对照补齐)。 + + 上游用会话数记账并强校验期望计数(R=4k、P=3k);本项目此前 + 只有美元门,在 unpriced 模型上成本恒为 0、门永远不触发。 + sessions 计探针会话与阶段尝试会话;edit() 内部的 fmt2 纠正 + 与选择/反思会话不计入(前者在接线层、后者不走 budget), + 上限语义因此是“受门控的会话数”,不是全轮任务总数。 + """ + max_cost_usd: float | None = None spent_usd: float = 0.0 + max_sessions: int | None = None + sessions: int = 0 @property def exceeded(self) -> bool: - return self.max_cost_usd is not None and self.spent_usd > self.max_cost_usd + if self.max_cost_usd is not None and self.spent_usd > self.max_cost_usd: + return True + return self.max_sessions is not None and self.sessions >= self.max_sessions def add(self, cost: float) -> None: self.spent_usd += float(cost or 0.0) + def add_session(self) -> None: + self.sessions += 1 + # --------------------------------------------------------------------------- # 阶段:一次提议(含一次修正机会)+ 一次探针 @@ -816,6 +833,18 @@ class ModeResult: if any(s.dim == "AGG" for s in self.stages) else None, "errors": self.errors, + # 探针结果此前不在 full_result 里:real5 的终版探针没建 + # 出来时,本地日志完全看不到原因,只能去服务端翻任务。 + # 只放摘要(不放 trajectory),避免日志爆炸。 + "rollouts": { + lbl: { + "ok": r.ok, + "delivered": r.delivered, + "error": (r.error or "")[:400], + "deliverable_text": (r.deliverable_text or "")[:500], + } + for lbl, r in self.rollouts.items() + }, } @@ -872,6 +901,11 @@ def _run_stage( err = "" spent = 0.0 for attempt in range(1, STAGE_ATTEMPTS + 1): + if budget is not None: + if budget.exceeded: + err = "budget exceeded before this attempt" + break + budget.add_session() try: result = editor.edit( full_mission + ( @@ -918,6 +952,8 @@ def _roll( if budget is not None and budget.exceeded: return Rollout(label=label, ok=False, delivered=False, error="budget exceeded before this rollout") + if budget is not None: + budget.add_session() try: rv = adapter.execute(label, tree) except Exception as exc: @@ -1164,8 +1200,11 @@ def select_final( f"{text.count(chr(10)) + 1:,} lines. COMPLETE -- nothing trimmed. -->\n") (cand_dir / f"{anon}.md").write_text(header + text, encoding="utf-8") ids = ", ".join(sorted(sel.id_map)) - if selector is None and k == 1: - # 唯一终版:不必再开一轮选择会话(上游的 trivial_selection)。 + if k == 1 and len(pool) == 1: + # 唯一终版:不必再开一轮选择会话(上游的 trivial_selection, + # 条件是“终版池 + k=1”,与选择器是否提供无关;此前误写成 + # selector is None 才 trivial,而接线永远传选择器,导致 + # k=1 也白白跑一个选择任务——2026-10-29 对照发现)。 label = next(iter(pool)) sel.ok = True sel.trivial = True @@ -1402,6 +1441,7 @@ def improve_task( library: Library | None = None, auto_apply: bool = False, budget_usd: float | None = None, + max_sessions: int | None = None, ) -> ImprovementResult: """跑一轮完整改进。base 探针先跑一次,作为全部候选共享的证据。 @@ -1413,7 +1453,7 @@ def improve_task( if not 1 <= int(k) <= MAX_K: raise ValueError(f"k must be 1..{MAX_K}, got {k!r}") work_dir = Path(work_dir) - budget = Budget(max_cost_usd=budget_usd) + budget = Budget(max_cost_usd=budget_usd, max_sessions=max_sessions) base_tree = adapter.base_harness() # 校验 base 本身:base 若已不合法,整轮的对比就没意义,直接报错 # 而不是让候选在坏 base 上各跑各的。 diff --git a/cloud/harness2_tasks.py b/cloud/harness2_tasks.py index 3061fa7..6e705e9 100644 --- a/cloud/harness2_tasks.py +++ b/cloud/harness2_tasks.py @@ -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 段表示 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", ] diff --git a/cloud/tests/test_harness2_tasks.py b/cloud/tests/test_harness2_tasks.py index 88f2a2c..f89411a 100644 --- a/cloud/tests/test_harness2_tasks.py +++ b/cloud/tests/test_harness2_tasks.py @@ -24,8 +24,8 @@ try: ) from cloud.harness2_tasks import ( CodexEditor, CodexProbeAdapter, HttpTaskClient, TaskState, - build_multipart, run_improvement_via_tasks, run_task, - unwrap_result_text, + build_multipart, preflight_checks, run_improvement_via_tasks, run_task, + unwrap_result_text, validate_run_tag, ) from cloud.uploads import parse_multipart, parse_paths_field except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path @@ -37,8 +37,8 @@ except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.p ) from cloud.harness2_tasks import ( CodexEditor, CodexProbeAdapter, HttpTaskClient, TaskState, - build_multipart, run_improvement_via_tasks, run_task, - unwrap_result_text, + build_multipart, preflight_checks, run_improvement_via_tasks, run_task, + unwrap_result_text, validate_run_tag, ) from cloud.uploads import parse_multipart, parse_paths_field @@ -211,7 +211,7 @@ class TestParseHelpers(unittest.TestCase): class TestFullRunViaTasks(unittest.TestCase): - def _run(self, client, run_tag="run1", work_sub="work"): + def _run(self, client, run_tag="run1", work_sub="work", k=1, max_sessions=None): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) root = Path(tmp.name) @@ -221,7 +221,8 @@ class TestFullRunViaTasks(unittest.TestCase): base_harness=dict(BASE_TREE), run_tag=run_tag, work_dir=root / work_sub, domain="accounting", source_files={"ledger.csv": LEDGER}, - k=1, mode="parallel", library_root=root / "library") + k=k, mode="parallel", library_root=root / "library", + max_sessions=max_sessions) return result, client, root def test_parallel_k1_via_real_task_shape(self): @@ -229,10 +230,11 @@ class TestFullRunViaTasks(unittest.TestCase): result, _, root = self._run(client) self.assertTrue(result.ok) self.assertEqual(result.selection.chosen_label, "agg1") + self.assertTrue(result.selection.trivial) # k=1 单终版不开选择会话 self.assertEqual(result.mode_result.rollouts["agg1"].deliverable_text, "Total: 85.15") - # 4 探针 + 3 编辑 + 1 选择 + 1 反思 = 9 个任务 - self.assertEqual(client.created, 9) + # 4 探针 + 3 编辑 + 1 反思 = 8 个任务(选择 trivial,不建任务) + self.assertEqual(client.created, 8) self.assertIsNotNone(result.library_entry) self.assertTrue((root / "library" / "accounting" / "INDEX.md").is_file()) @@ -253,8 +255,9 @@ class TestFullRunViaTasks(unittest.TestCase): self.assertIsNone(result.library_entry) def test_selector_gibberish_falls_back(self): + # k=1 时选择 trivial,不走选择器;用 k=2 让选择器真正跑起来。 client = FakeTaskClient(selector_mode="gibberish") - result, _, _ = self._run(client) + result, _, _ = self._run(client, k=2) self.assertTrue(result.ok) self.assertTrue(result.selection.fallback) @@ -338,6 +341,79 @@ class TestEnvelopeFullRound(unittest.TestCase): self.assertIsNotNone(result.library_entry) +class TestSessionBudget(unittest.TestCase): + """会话数门:0 挡全部、2 挡在中途;fmt2 与选择/反思不计入。""" + + def _run(self, max_sessions): + client = FakeTaskClient() + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + root = Path(tmp.name) + result = run_improvement_via_tasks( + client, project_id="p1", task_id="ledger-1", + prompt="Sum every signed amount in ledger.csv. Return Total: with two decimals.", + base_harness=dict(BASE_TREE), run_tag="sess", + work_dir=root / "work", domain="accounting", + source_files={"ledger.csv": LEDGER}, + k=1, mode="parallel", library_root=root / "library", + max_sessions=max_sessions) + return result, client + + def test_zero_blocks_everything(self): + result, client = self._run(0) + self.assertEqual(client.created, 0) + self.assertEqual(set(result.mode_result.fell_back), {"inv1", "dom1", "agg1"}) + + def test_mid_cap_stops_after_two_sessions(self): + # base 探针(1)+ inv1 编辑(2),之后 dom1/agg1 与探针全被门挡住。 + result, client = self._run(2) + self.assertEqual(client.created, 2) + self.assertIn("dom1", result.mode_result.fell_back) + self.assertIn("agg1", result.mode_result.fell_back) + self.assertNotIn("inv1", result.mode_result.fell_back) + + +class TestValidateRunTag(unittest.TestCase): + def test_valid(self): + for tag in ("h2-20261029-real1", "a", "a.b_c-1", "run_1"): + validate_run_tag(tag) + + def test_invalid(self): + for tag in ("-bad", "has/slash", "has space", "s1-foo", "s12"): + with self.assertRaises(ValueError, msg=tag): + validate_run_tag(tag) + validate_run_tag("") # 空串表示稍后生成,不校验 + + +class TestPreflight(unittest.TestCase): + def _dirs(self, agents="Use scripts/summarize.py.\n"): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + root = Path(tmp.name) + (root / "base").mkdir() + if agents is not None: + (root / "base" / "AGENTS.md").write_text(agents) + (root / "source").mkdir() + (root / "source" / "ledger.csv").write_bytes(LEDGER) + return root + + def test_ok(self): + root = self._dirs() + errors, _ = preflight_checks( + base_harness_dir=root / "base", source_dir=root / "source", + prompt="do it", run_tag="h2-ok") + self.assertEqual(errors, []) + + def test_errors(self): + root = self._dirs(agents="") + errors, _ = preflight_checks( + base_harness_dir=root / "base", source_dir=root / "missing", + prompt=" ", run_tag="s1-bad") + self.assertEqual(len(errors), 4) # AGENTS 为空、source 缺失、提示为空、tag 非法 + errors, _ = preflight_checks(base_harness_dir=root / "nope", prompt="x") + self.assertTrue(any("不存在" in e for e in errors)) + + class TestHttpClient(unittest.TestCase): def test_client_against_local_server(self): seen = {"keys": [], "uploads": [], "tasks": []} diff --git a/docs/harness2.md b/docs/harness2.md index 5818eb9..bbf1249 100644 --- a/docs/harness2.md +++ b/docs/harness2.md @@ -30,9 +30,13 @@ benchmark 上的,不是本项目的承诺——本项目没有 benchmark,接 |---|---| | c1–c8 可编辑树(codex surface) | 照搬:AGENTS.md(8000 字符上限)、`.agents/skills/*/SKILL.md`、`.agents/skills/*/scripts/*`、`sub_agents/*/AGENT.md`、`scripts/*.py`、`.codex/hooks.json`;白名单外一律不读不写;校验含技能 description、触发行、脚本 compile、脚本引用存在、钩子 JSON 与事件名 | | Propose–Probe–Compose,parallel / sequential | 照搬语义,改写实现:`run_parallel` / `run_sequential`,提议被拒给一次按反馈修正的机会,再拒回落到当前 harness;探针失败记为死探针,不炸整轮 | -| 匿名选择(只比产物) | 照搬:终版产物按任务+k+模式定种子洗牌匿名化,选择器看不到 harness 来源;选失败回落到已交付终版;k=1 无选择器时 trivial 直出 | +| 匿名选择(只比产物) | 照搬:终版产物按任务+k+模式定种子洗牌匿名化,选择器看不到 harness 来源;选失败回落到已交付终版;k=1 且终版池只有唯一候选时 trivial 直出(与选择器是否提供无关;2026-10-29 对照纠正了此前“无选择器才 trivial”的误写) | | 经验库(分领域、整树 + EXPERIENCE.md + INDEX.md) | 照搬存储格式,改写生效语义:入库 ≠ 自动生效(见下) | -| Ledger(会话/探针账本)与成本 | 改写:成本汇总进 `ModeResult.total_cost_usd`,由调用方接现有 `cloud/cost.py` / 租户配额计费;另有 `budget_usd` 超支门:超支后不再调用编辑器/探针,剩余阶段回落,且该轮不入库 | +| Ledger(会话/探针账本)与成本 | 改写:成本汇总进 `ModeResult.total_cost_usd`,由调用方接现有 `cloud/cost.py` / 租户配额计费;另有 `budget_usd` 超支门与 `max_sessions` 会话数门(计探针 + 阶段尝试会话;edit 内 fmt2 纠正与选择/反思会话不计入):任一超支后不再调用编辑器/探针,剩余阶段回落,且该轮不入库;上游的 R=4k/P=3k 事后强校验未引(本项目结构与上游计数不完全同构,门控代替断言) | +| run_tag 形状与建任务前预检 | 对照补齐:`validate_run_tag`(字符集、首字符不为 -、不以 s 数字段开头)与 `preflight_checks`(base 目录与 AGENTS.md、source 目录、提示、tag、控制面 health 探针);CLI `--check` 只预检不建任务 | +| 分角色模型(SOLVER / IMPROVER / JUDGE) | 未引:全部角色共用调用方透传的同一个 model;需用户定各角色模型才有意义 | +| 产物资产(deliverable_files / deliverable_assets / images)与选择 assets note | 未引:探针交付物目前只有最终回复文本;需新增 worktree 文件读取端点后与该端点一起做 | +| sweep 层(多任务批量、跨任务 library restore、dead 隔离、report 汇总) | 未引:单任务产品形态用不上,批量改进需求出现再做 | | 提示词 | 改写短版(中文项目语境下的英文任务提示,结构同上游,措辞自写),不抄上游长提示 | | Benchmark 适配器(JobBench / LAB / WorkBuddy)、评分器、rubric | 不引。任务就是一个普通 codex 任务,delivered 由 adapter 定义(如产物存在且非空) | | Proposer 通道(Claude CLI / OpenCode)、Vertex 配置 | 不引。编辑器/选择器/反思器是调用方注入的协议,真实接线时是 codex 任务(见下) | diff --git a/examples/harness2_run.py b/examples/harness2_run.py index 1c994be..a90d121 100644 --- a/examples/harness2_run.py +++ b/examples/harness2_run.py @@ -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)