diff --git a/cloud/harness2.py b/cloud/harness2.py index d3210ba..f74c67f 100644 --- a/cloud/harness2.py +++ b/cloud/harness2.py @@ -241,6 +241,56 @@ def parse_harness_files(text: str) -> dict[str, str]: 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 # 提议被拒后给一次按反馈修正的机会,再拒就回落 @@ -490,6 +540,7 @@ class Rollout: trajectory: str = "" cost_usd: float = 0.0 error: str = "" + deliverable_files: dict[str, str] = field(default_factory=dict) @dataclass @@ -589,7 +640,12 @@ def build_context( (dd / f"{r.label}.md").write_text( r.deliverable_text or "(no deliverable)", encoding="utf-8" ) - lines.append("context/deliverables/ what each rollout produced") + 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" + " (+