"""Harness² 式递归改进,融合进 codex-app-server 的方法层。 来源:Google Research 的 harness2(Apache 2.0,论文 arXiv:2610.14224), 只引它的方法,不引它的部署默认。本模块是按上游思想重写的适配实现, 不是把上游代码原样拷进来:接口按 codex-app-server 的任务/成本模型改过, 提示词是按本项目重写的短版,上游的长提示词、benchmark 适配器、OpenCode / Claude CLI 通道都没有搬。 一句话:模型权重不动,只改模型周围的 harness(系统提示、技能、脚本、 子代理、钩子),用“提议—探针—合成”循环拿行为证据说话,再把被采纳的 经验写进分领域的经验库,供后面的任务参考。 核心循环(上游叫 Propose-Probe-Compose): 1. Propose:对同一任务并行提两类候选 harness——INV(通用做法:流程、 检查、工具用法)与 DOM(领域做法:该行当的专业知识)。 2. Probe:每个候选 harness 都在同一任务上真实跑一次(探针),只看 行为对比,不看任何标准答案——没有标准答案可对,这是开世界任务 的前提。 3. Compose:合成器读全部探针轨迹,把有行为证据支撑的改动合成 一个终版 harness;没有证据的改动不进终版。 两种放量方式(上游的 parallel / sequential): - parallel:一次提 2k 个候选,探针后合成 k 个终版,探得广; - sequential:k 轮,每轮提 INV+DOM 各 1 个、合成一个检查点, 下一轮在检查点上继续改,改得深。 选终版时只比“产物”:候选产物先匿名化(A/B/C……,按任务+k+模式 定种子洗牌,可复现),选择器看不到哪个产物出自哪个 harness;选不 出来时回落到已交付的终版,绝不抛异常把整轮炸掉。k=1 且终版池 只有唯一候选时直接走 trivial:唯一的终版就是答案,不必再选 一次(与选择器是否提供无关)。 经验库(上游叫 harnessing-level recursion): 采纳后,反思器只写一条有证据的经验(cue / lesson / evidence 三项 齐全才入库,缺一项就不存),连同被采纳 harness 的完整树一起存进 //harnesses//,并重建 INDEX.md。下个同领域 任务的提议器能读到这些条目——但注意本项目的默认:入库不等于自动 生效(见下文安全偏差)。 与上游刻意不同的地方(都是为了不破坏本项目已有的安全与计费): 1. 不抄上游的执行默认。上游 README 明写:执行器网络不受限, Codex 以 sandbox 禁用方式跑。本模块不跑任何命令、不拼任何 codex exec 参数;执行全部走调用方提供的 adapter,而本项目的 真实 adapter 必须走 Worker 已有的 Docker / gVisor 沙箱、租户 隔离与 egress 白名单,绝不加 --dangerously-bypass-*。 2. 不自动把采纳的 harness 铺到别的任务。codex-app-server 是多租户 服务,一个编辑器写出的 AGENTS.md 自动改写别人的任务既是质量 风险也是越权风险。所以 library 默认只是“待评审的经验”:只有 项目显式开启 auto_apply(后续服务端接线时再加开关,默认关), 下个任务的 base harness 才会从库里取最新采纳树;默认情况下 base 永远是项目自己的 base harness,库只给提议器当参考。 3. 不引 benchmark。任务就是 codex-app-server 里一个普通任务 (prompt + 产物),没有 JobBench / LAB / WorkBuddy 适配器, 没有评分器、没有 rubric;delivered 的定义由 adapter 说了算 (例如“产物文件存在且非空”),选择器只按产物本身判断。 4. 成本有上限。k 硬上限 3(parallel 一轮最多 1 个 base + 2k 候选 + k 终版 = 10 次执行;sequential 最多 1 + 3k = 10 次),另有 可选的 budget_usd:超支后编辑器不再被调用、探针不再执行, 剩余阶段直接回落到当前 harness,采纳结果也不入库——超支的 轮次不配进经验库。 5. 编辑器/选择器/反思器不是本模块内置的模型调用,而是调用方注入 的三个协议(Editor / Selector / Reflector)。真实接线见 cloud/harness2_tasks.py:它们各自是一个 codex-app-server 任务, prompt 就是本模块生成的 mission,证据以附件 seed 进任务 worktree(context/ 前缀),编辑器把改动的文件以文件块形式写 在最终回复里由接线层解析回树;选择器与反思器同理,只解析 最终回复,不读 worktree 文件(跨主机读不到,见 docs/harness2.md)。 可编辑面(上游 codex substrate 的 surface,原样照搬): - AGENTS.md:常驻文件,每回合都注入,硬上限 8000 字符(上游 注明 codex 还会在 32768 处静默截断,8000 是留足余量的做法)。 - .agents/skills//SKILL.md:技能,frontmatter 必须有 description,否则加载时会被静默丢弃;且必须在 AGENTS.md 里 有触发行,否则技能永远不会被调用(本模块会自动补触发行, 补不下——会超 8000 上限——时跳过,不硬塞)。 - sub_agents//AGENT.md:子代理角色,同样需要 description。 - scripts/*.py:确定性辅助脚本,必须能通过 compile 检查; AGENTS.md / 技能里引用的脚本必须真实存在于树里。 - .codex/hooks.json:生命周期钩子,只接受框架实际派发的事件名, 且钩子必须是 command 类型、命令非空(格式不对的钩子在框架侧 是静默不触发,比报错更危险,所以在提议阶段就拦住)。 不在上述白名单里的路径一律不读、不写、不采纳;想逃出树外的 路径(..、绝对路径)在规范化时直接拒绝。 用法(确定性演示见 examples/harness2_demo.py,不花模型钱): result = improve_task(adapter, editor, selector, reflector, work_dir=Path("runs/demo"), k=1, mode="parallel") """ from __future__ import annotations import hashlib import json import random import re import shutil from dataclasses import dataclass, field from pathlib import Path from typing import Protocol, Sequence # --------------------------------------------------------------------------- # 可编辑面:codex substrate # --------------------------------------------------------------------------- SKILL_DIR = ".agents/skills" SUBAGENT_DIR = "sub_agents" SCRIPT_DIR = "scripts" HOOKS_FILE = ".codex/hooks.json" ALWAYS_ON = ("AGENTS.md",) ALWAYS_ON_MAX = 8000 HOOK_EVENTS = frozenset( { "PreToolUse", "PostToolUse", "PermissionRequest", "PreCompact", "PostCompact", "SessionStart", "SubagentStart", "SubagentStop", "UserPromptSubmit", "Stop", } ) _NAME = r"[a-z0-9][a-z0-9_\-]*" _SKILL_RX = re.compile(rf"{re.escape(SKILL_DIR)}/({_NAME})/SKILL\.md") _SKILL_SCRIPT_RX = re.compile(rf"{re.escape(SKILL_DIR)}/{_NAME}/scripts/[A-Za-z0-9_.\-]+") _SUBAGENT_RX = re.compile(rf"{SUBAGENT_DIR}/({_NAME})/AGENT\.md") _SCRIPT_RX = re.compile(r"scripts/[a-z0-9_][a-z0-9_]*\.py") _SCRIPT_REF_RX = re.compile(rf"(?:python3?\s+)((?:{re.escape(SKILL_DIR)}/[\w\-]+/)?scripts/[\w.\-]+\.py)") _CHOICE_RX = re.compile(r"```choice\s*\n?\s*([A-Za-z0-9_-]+)\s*\n?\s*```", re.S) _WHY_RX = re.compile(r"(.*?)", re.S) _NOTES_RX = re.compile(r"(.*?)", re.S) _NO_CHANGE_RX = re.compile(r"(.*?)", re.S) _EXP_RX = re.compile(r"(.*?)", re.S) _EDITS_RX = re.compile(r"(.*?)", re.S) Tree = dict[str, str] # 编辑任务最终回复里的文件块格式(接线层 cloud/harness2_tasks.py 与 # 编辑任务的 prompt 共用这一格式,改一处就要同步另一处): # === HARNESS FILE: <白名单内的相对路径> === # <该文件的完整内容> # === END HARNESS FILE === # 只输出改动的文件,没改的文件省略即继承(上游同款“省略即继承” # 契约)。文件内容里若恰好有整行 === END HARNESS FILE === 会被 # 提前截断——极罕见,校验与人工复核是兜底,见 docs/harness2.md。 _HARNESS_FILE_START_RX = re.compile(r"^=== HARNESS FILE:\s*(.+?)\s*===$") _HARNESS_FILE_END = "=== END HARNESS FILE ===" def parse_choice(text: str) -> str | None: """从选择器最终回复里取 ```choice 块中的匿名 id;没有返回 None。""" m = _CHOICE_RX.search(text or "") return m.group(1).strip() if m else None def parse_why(text: str) -> str: m = _WHY_RX.search(text or "") return m.group(1).strip() if m else "" def parse_notes(text: str) -> str: m = _NOTES_RX.search(text or "") return m.group(1).strip() if m else "" def parse_no_change(text: str) -> str: """从编辑任务最终回复里取 标记中的理由;没有返回 ""。 2026-10-29 两轮真实轮次的教训:编辑在 里声称“已修 正”,却一个文件块都没发,旧契约把这种 notes-only 回复当成 “无需改动”采纳,导致改动无声丢失。从本版起,无改动必须用 这个显式标记表态,notes-only 不再算成功(见接线层 edit 的 格式重试);标记与文件块同时出现属于自相矛盾,同样算格式错。 """ m = _NO_CHANGE_RX.search(text or "") return m.group(1).strip() if m else "" def parse_experience(text: str) -> dict: """从反思器最终回复里取 与 。 cue / lesson / evidence 三项不全,或回复写明 NO_ENTRY 时返回 {}, 表示没有可入库的经验——调用方不得把半条经验写进库。 """ text = text or "" if "NO_ENTRY" in text: return {} m = _EXP_RX.search(text) if not m: return {} out: dict = {} for line in m.group(1).strip().splitlines(): if ":" in line: key, _, val = line.partition(":") key = key.strip().lower() if key in ("cue", "lesson", "evidence", "confidence"): out[key] = val.strip() if not all(out.get(k) for k in ("cue", "lesson", "evidence")): return {} edits = _EDITS_RX.search(text) if edits: out["edits"] = edits.group(1).strip() return out def parse_harness_files(text: str) -> dict[str, str]: """解析编辑任务最终回复里的文件块,返回 {相对路径: 完整内容}。 路径不在可编辑白名单上时直接抛 ValueError——接线层把这当成 该阶段失败(重试后回落),绝不静默丢掉一块就当没这回事。 同一路径出现两次时后一块覆盖前一块,与最终回复“最后一次 表态为准”的直觉一致。 """ out: dict[str, str] = {} cur_path: str | None = None cur_lines: list[str] = [] for line in (text or "").splitlines(): m = _HARNESS_FILE_START_RX.match(line.strip()) if m: if cur_path is not None: raise ValueError(f"harness file block for {cur_path!r} was not closed") cur_path = m.group(1).strip() if not is_allowed(cur_path): raise ValueError(f"harness file path is off the editable surface: {cur_path!r}") cur_lines = [] continue if line.strip() == _HARNESS_FILE_END: if cur_path is None: raise ValueError("stray === END HARNESS FILE === without a start line") out[cur_path] = "\n".join(cur_lines) + ("\n" if cur_lines else "") cur_path = None cur_lines = [] continue if cur_path is not None: cur_lines.append(line) if cur_path is not None: raise ValueError(f"harness file block for {cur_path!r} was not closed") return out _DELIVERABLE_FILE_START_RX = re.compile(r"^=== DELIVERABLE FILE:\s*(.+?)\s*===$") _DELIVERABLE_FILE_END = "=== END DELIVERABLE FILE ===" _DELIVERABLE_FILE_MAX = 8 _DELIVERABLE_FILE_MAX_CHARS = 65536 def _is_safe_deliverable_path(path: str) -> bool: if not path or path.startswith("/") or "\x00" in path: return False return all(p not in ("", ".", "..") for p in path.split("/")) def parse_deliverable_files(text: str) -> dict[str, str]: """解析探针回复里的产物文件块,返回 {相对路径: 内容}。 与编辑文件块的严格语义不同:这里故意宽容——坏块(未闭合、 非法路径、超量、超长)只丢弃该块、不抛错,因为产物文件是 交付文本之外的 enrichment,绝不能因为一块坏内容毁掉整轮 交付物。超长内容截断并打截断标记,让评审知道它不完整; 超过 8 块时只取前 8 块。同一路径出现两次时后一块覆盖前一块。 """ out: dict[str, str] = {} cur_path: str | None = None cur_lines: list[str] = [] for line in (text or "").splitlines(): m = _DELIVERABLE_FILE_START_RX.match(line.strip()) if m: if cur_path is not None: cur_path = None cur_lines = [] path = m.group(1).strip() if _is_safe_deliverable_path(path) and len(out) < _DELIVERABLE_FILE_MAX: cur_path = path cur_lines = [] continue if line.strip() == _DELIVERABLE_FILE_END: if cur_path is not None: content = "\n".join(cur_lines) + ("\n" if cur_lines else "") if len(content) > _DELIVERABLE_FILE_MAX_CHARS: content = (content[:_DELIVERABLE_FILE_MAX_CHARS] + "\n...[truncated]") out[cur_path] = content cur_path = None cur_lines = [] continue if cur_path is not None: cur_lines.append(line) return out MAX_K = 3 STAGE_ATTEMPTS = 2 # 提议被拒后给一次按反馈修正的机会,再拒就回落 def normalize_rel(rel: str) -> str: """规范化树内相对路径;逃出树外的路径直接拒绝。""" r = str(rel).strip().replace("\\", "/") if r.startswith("/") or ".." in r.split("/"): raise ValueError(f"path escapes the harness: {rel!r}") while r.startswith("./"): r = r[2:] return r.strip("/") def is_allowed(rel: str) -> bool: """该路径是否在可编辑白名单上(永不抛异常,只回答是/否)。""" try: rel = normalize_rel(rel) except ValueError: return False if rel in ALWAYS_ON or rel == HOOKS_FILE: return True return bool( _SKILL_RX.fullmatch(rel) or _SKILL_SCRIPT_RX.fullmatch(rel) or _SUBAGENT_RX.fullmatch(rel) or _SCRIPT_RX.fullmatch(rel) ) def skill_names(tree: Tree) -> list[str]: return sorted({m.group(1) for rel in tree if (m := _SKILL_RX.fullmatch(rel))}) def subagent_names(tree: Tree) -> list[str]: return sorted({m.group(1) for rel in tree if (m := _SUBAGENT_RX.fullmatch(rel))}) def always_on_text(tree: Tree) -> str: return "\n\n".join(tree.get(a, "") for a in ALWAYS_ON) def read_tree(d: Path | str, *, rejected: list[str] | None = None) -> Tree: """读一个目录里的可编辑树;白名单外的文件记入 rejected,不读进来。""" d = Path(d) out: Tree = {} if not d.is_dir(): return out for p in sorted(d.rglob("*")): if not p.is_file() or "__pycache__" in p.parts: continue rel = str(p.relative_to(d)) if not is_allowed(rel): if rejected is not None: rejected.append(rel) continue out[rel] = p.read_text(encoding="utf-8", errors="replace") return out def write_tree(tree: Tree, dest: Path | str) -> Path: """把一棵树原样落盘:先清掉目标里旧的可编辑文件,再写新文件。 目标里白名单外的文件不动——harness 目录里可能还放着任务产物, 不能因为换 harness 就删掉它们。 """ dest = Path(dest) dest.mkdir(parents=True, exist_ok=True) for p in sorted(dest.rglob("*"), reverse=True): if p.is_file() and is_allowed(str(p.relative_to(dest))): p.unlink() for rel, content in tree.items(): p = dest / normalize_rel(rel) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") return dest def fingerprint(tree: Tree) -> str: """一棵树的摘要:排序后的 路径\\0内容 拼起来算 sha256,前 16 位。""" h = hashlib.sha256() for rel in sorted(tree): h.update(rel.encode()) h.update(b"\0") h.update(tree[rel].encode("utf-8", errors="replace")) h.update(b"\0") return h.hexdigest()[:16] def autotrigger(tree: Tree) -> list[str]: """给每个技能在 AGENTS.md 里补触发行;会超 8000 上限的行跳过。 返回补了触发行的技能名。触发行的描述取技能 frontmatter 的 description 第一行,取不到时用通用措辞。 """ fixed: list[str] = [] for name in skill_names(tree): if name in always_on_text(tree): continue fm = tree.get(f"{SKILL_DIR}/{name}/SKILL.md", "")[:600] m = re.search(r"^description:\s*(.+)$", fm, re.M) desc = m.group(1).strip().rstrip(".") if m else f"the {name} workflow applies" line = f"- When {desc}, read {SKILL_DIR}/{name}/SKILL.md and follow it.\n" if len(always_on_text(tree)) + len(line) + 2 > ALWAYS_ON_MAX: continue cur = tree.get("AGENTS.md", "") tree["AGENTS.md"] = (cur.rstrip() + "\n" + line) if cur.strip() else line fixed.append(name) return fixed def _check_hooks(src: str, errs: list[str]) -> None: try: doc = json.loads(src) except json.JSONDecodeError as exc: errs.append(f"{HOOKS_FILE} is not valid JSON: {exc}") return hooks = doc.get("hooks") if isinstance(doc, dict) else None if not isinstance(hooks, dict) or not hooks: errs.append(f'{HOOKS_FILE} needs a top-level {{"hooks": {{"": [...]}}}} object') return for event, entries in hooks.items(): if event not in HOOK_EVENTS: errs.append(f"{HOOKS_FILE}: unknown event {event!r}; dispatched events are " f"{', '.join(sorted(HOOK_EVENTS))} (PascalCase)") continue if not isinstance(entries, list): errs.append(f"{HOOKS_FILE}: {event} must map to a LIST of matcher entries") continue for i, entry in enumerate(entries): if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list): errs.append(f"{HOOKS_FILE}: {event}[{i}] needs a `hooks` list") continue for h in entry["hooks"]: if not isinstance(h, dict) or h.get("type") != "command": errs.append(f'{HOOKS_FILE}: {event}[{i}] hook type must be "command"') elif not str(h.get("command", "")).strip(): errs.append(f"{HOOKS_FILE}: {event}[{i}] has an empty `command`") def validate(tree: Tree, *, reserved_scripts: frozenset[str] = frozenset()) -> dict: """校验一棵树;有问题就抛 ValueError,把全部问题一次列清。 返回值是树的普查(文件数、技能数、子代理数、脚本数、常驻字符数), 供日志与测试断言用。校验失败时不返回普查——调用方应按“提议被拒” 处理,而不是拿半棵树继续跑。 """ errs: list[str] = [] body = always_on_text(tree) if len(body) > ALWAYS_ON_MAX: errs.append(f"always-on content is {len(body)} chars, over the {ALWAYS_ON_MAX} cap; " "AGENTS.md is injected whole into EVERY turn") if not body.strip(): errs.append("AGENTS.md is empty — it is the only always-on file, " "so an empty one is a harness that does not exist") for name in skill_names(tree): fm = tree[f"{SKILL_DIR}/{name}/SKILL.md"][:600] if not re.search(r"^description:\s*\S", fm, re.M): errs.append(f"{SKILL_DIR}/{name}/SKILL.md needs a `description:` in its " "frontmatter, or the skill is dropped SILENTLY at load") m_name = re.search(r"^name:\s*(\S+)\s*$", fm, re.M) if m_name and m_name.group(1) != name: errs.append(f"{SKILL_DIR}/{name}/SKILL.md frontmatter `name: " f"{m_name.group(1)}` OVERRIDES the directory name at load, so " f"every trigger line naming `{name}` stops matching") if name not in body: errs.append(f"skill '{name}' is never named in the always-on text, so the " "agent will not know to invoke it — an untriggered skill is " "dead weight") for name in subagent_names(tree): fm = tree[f"{SUBAGENT_DIR}/{name}/AGENT.md"][:600] if not re.search(r"^description:\s*\S", fm, re.M): errs.append(f"{SUBAGENT_DIR}/{name}/AGENT.md needs a `description:`") for rel, src in tree.items(): if _SCRIPT_RX.fullmatch(rel) or ( _SKILL_SCRIPT_RX.fullmatch(rel) and rel.endswith(".py") ): try: compile(src, rel, "exec") except SyntaxError as exc: errs.append(f"{rel} does not compile: {exc.msg} (line {exc.lineno})") if HOOKS_FILE in tree: _check_hooks(tree[HOOKS_FILE], errs) have = {rel for rel in tree if _SCRIPT_RX.fullmatch(rel) or _SKILL_SCRIPT_RX.fullmatch(rel)} for rel, src in tree.items(): if not (_SKILL_RX.fullmatch(rel) or rel in ALWAYS_ON): continue for ref in _SCRIPT_REF_RX.findall(src): if ref.rsplit("/", 1)[-1] in reserved_scripts: continue if ref not in have: errs.append(f"{rel} invokes `{ref}` but that file is not in the harness") if errs: raise ValueError(" | ".join(errs)) return { "always_on_chars": len(body), "n_files": len(tree), "skills": len(skill_names(tree)), "sub_agents": len(subagent_names(tree)), "scripts": sum(1 for rel in tree if _SCRIPT_RX.fullmatch(rel)), "hooks": 1 if HOOKS_FILE in tree else 0, } def diff_summary(before: Tree, after: Tree) -> dict: return { "added": sorted(set(after) - set(before)), "removed": sorted(set(before) - set(after)), "modified": sorted(rel for rel in set(before) & set(after) if before[rel] != after[rel]), "always_on_char_delta": len(always_on_text(after)) - len(always_on_text(before)), } def diversity(trees: dict[str, Tree]) -> dict: """候选树之间的两两行 Jaccard 相似度:全塌成同一棵树时能看出来。""" names = sorted(trees) if len(names) < 2: return {"n": len(names), "n_distinct": len(names), "mean_line_jaccard": 1.0} def lines(t: Tree) -> set[str]: return {f"{rel}:{ln}" for rel, c in t.items() for ln in c.splitlines() if ln.strip()} sets = {n: lines(trees[n]) for n in names} sims = [] for i, a in enumerate(names): for b in names[i + 1:]: u = sets[a] | sets[b] sims.append(len(sets[a] & sets[b]) / len(u) if u else 1.0) return { "n": len(names), "n_distinct": len({tuple(sorted(t.items())) for t in trees.values()}), "mean_line_jaccard": round(sum(sims) / len(sims), 4), } # --------------------------------------------------------------------------- # 协议:执行 / 编辑 / 选择 / 反思,全部由调用方注入 # --------------------------------------------------------------------------- @dataclass class Rollout: label: str ok: bool delivered: bool deliverable_text: str = "" trajectory: str = "" cost_usd: float = 0.0 error: str = "" deliverable_files: dict[str, str] = field(default_factory=dict) @dataclass class EditResult: """一次编辑会话的结果。tree 为 None 时,调用方应从 context_dir/harness 里把编辑器改完的树读回(真实 codex 任务接线就是这个方式)。""" tree: Tree | None notes: str = "" cost_usd: float = 0.0 cached: bool = False class TaskAdapter(Protocol): """一个任务的执行面:base harness 从哪来、一次探针怎么跑。""" task_id: str domain: str prompt: str def base_harness(self) -> Tree: """未改动的 base harness。""" ... def execute(self, label: str, harness: Tree) -> Rollout: """用给定 harness 跑一次任务;失败也返回 Rollout,永不抛异常 (抛了也由 _roll 兜住,记为一次死探针)。""" ... def validate_tree(self, harness: Tree) -> None: """项目特有的额外不变式;默认什么都不拦。""" ... class Editor(Protocol): def edit(self, mission: str, context_dir: Path, label: str) -> EditResult: """读 context_dir 里的证据(task.md / rollouts / deliverables / harness / library),改 context/harness/,返回改完的树。""" ... class Selector(Protocol): def select(self, mission: str, context_dir: Path) -> tuple[str, str, float]: """返回 (匿名候选 id, 选择理由, 花费 USD)。id 必须是 mission 里 列出的 A/B/C…… 之一,否则按选择失败回落。""" ... class Reflector(Protocol): def reflect(self, mission: str, context_dir: Path) -> dict: """返回经验 dict,键至少含 cue / lesson / evidence,可含 confidence / edits;三项不全时返回 {} 表示没有可入库的经验。""" ... def _adapter_validate(adapter: TaskAdapter, tree: Tree) -> None: fn = getattr(adapter, "validate_tree", None) if callable(fn): fn(tree) # --------------------------------------------------------------------------- # 证据上下文:给模型看的文件,而不是把证据塞进 prompt # --------------------------------------------------------------------------- def build_context( root: Path, *, task_prompt: str, rollouts: Sequence[Rollout], harness: Tree, library_dir: Path | None = None, pool: Sequence[tuple[str, Path, Rollout | None]] = (), decisions: dict[str, str] | None = None, ) -> str: """在 root 下建 context/ 证据树,返回 mission 里用的布局说明。 上游的关键做法照搬:证据以文件形式给,prompt 只写指针,这样 证据不会为了塞进 prompt 而被截断——截断过的轨迹会编造缺陷。 """ root = Path(root) root.mkdir(parents=True, exist_ok=True) (root / "task.md").write_text(task_prompt, encoding="utf-8") lines = ["context/task.md the task as the agent received it"] if rollouts: d = root / "rollouts" d.mkdir(parents=True, exist_ok=True) for r in rollouts: (d / f"{r.label}.md").write_text(r.trajectory or "(no trajectory)", encoding="utf-8") names = ", ".join(f"{r.label}.md" for r in rollouts[:6]) more = f" (+{len(rollouts) - 6} more)" if len(rollouts) > 6 else "" lines.append(f"context/rollouts/ FULL trajectories: {names}{more}") dd = root / "deliverables" dd.mkdir(parents=True, exist_ok=True) for r in rollouts: (dd / f"{r.label}.md").write_text( r.deliverable_text or "(no deliverable)", encoding="utf-8" ) for name, content in (r.deliverable_files or {}).items(): dest = dd / r.label / "files" / name dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(content, encoding="utf-8") lines.append("context/deliverables/ what each rollout produced" " (+