Skip to content

Commit ff7cb07

Browse files
committed
沙盒产物新建和改写都采集:运行前后比对文件大小与修改时间,回退重做那一轮重写上一轮同名的图 / 结果表照样采集;节点侧同名产物按(kind, 文件名)只留一份——一波之内取最后一次运行的、跨波取采用波那版;沙盒报告的指标来源取最后一次运行的 metrics.json
起因:v3.61 读码发现沙盒只采集本次运行新建的文件(runners._collect_artifacts 按运行前后的文件集合求差)。 回退重做那一轮重写上一轮同名的图 / 结果表一个引用都没有,实验第 2 轮沿用上一轮图名时 figures 清单缺图、论文 图件清单随之少图;同一步骤里第二次运行重写的文件也不再采集,节点产物一直指向第一次运行那版内容。 改动 - tools runners.py:_file_states 在运行前(脚本落盘之后)与运行后各记一次每个文件的(大小, mtime_ns),新出现 或变了的都采集;MAX_ARTIFACTS / MAX_ARTIFACT_BYTES、按后缀归类与存储不变。没动过的文件(下发数据、上一轮的 其它产物、存储副本)不采,原样内容再写一遍也算改写。python_run 与 code_run 共用这一个执行核,R 同样生效。 python_runner.py 模块说明同步。 - skills nodes.py:同一文件现在每跑一次报一版,节点侧按 _artifact_key(kind, 文件名——ArtifactRef 上只剩 basename,与图件清单、图件编号、界面产物名同一口径)只留一份。_SandboxCapture.add 一波之内后一次运行顶替 前一次、位置不变,同一次运行里重名的取第一个(沿用「同名重复采集只算一次」);_union_artifacts 跨波取采用波 那版(与采用的指标对得上),采用波没有的取最后报到的那波,code 只留采用波——此前按 uri 去重,内容一变两版 都留、图件清单取到旧版。_figures_rendered_check 口径改为本波运行新建或改写过;补图修复波仍换 figures/rev<N>/, 理由改为僵持回落时盘上文件与采用的产物是同一版。 - harness sandbox_agent.py:metrics_source_artifact 取最后一次运行的 metrics.json(此前取第一个,改写也采集后 就是最早那份旧版);produced_artifacts 照契约列全部 id。 测试(本地测试目录不入库) - tools +1:重做轮(另一步骤)重写图与结果表 → 只采这两个、内容是新的、producer_step 是本轮;原样再写也采、 只读不采。skills +2:实验节点一波两次运行都报 fit.png → 产物与图件清单取后一版、位置不变;_SandboxCapture 顶替与同次重名,_union_artifacts 采用修复波 / 僵持回落首波 / 无采用波三态。harness +1:指标来源取后一次、 produced_artifacts 两版都列。新用例在 HEAD 版源码上全红。 - agents 五包 + worker 830 passed / 1 skipped,backend/api 515 passed / 2 skipped;ruff 对照 HEAD 无新增告警, mypy 全量与 HEAD 逐条相同。
1 parent 8c0ab63 commit ff7cb07

4 files changed

Lines changed: 75 additions & 35 deletions

File tree

‎agents/harness/src/omm_agent_harness/sandbox_agent.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,10 +454,11 @@ def evidence() -> SandboxEvidence:
454454
if publish_code is not None and tracker.last_code:
455455
final_code_artifact = publish_code(tracker.last_code)
456456

457+
# 改写过的文件每次运行都会再报一版:取最后一次运行的那份(工作区里就是它)
457458
metrics_source = next(
458459
(
459460
artifact_id
460-
for artifact_id, name in tracker.artifact_names.items()
461+
for artifact_id, name in reversed(tracker.artifact_names.items())
461462
if name == "metrics.json"
462463
),
463464
None,

‎agents/skills/src/omm_agent_skills/nodes.py‎

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,21 +1799,40 @@ def language_env_fingerprint(probe: Mapping[str, Any], language: str) -> dict[st
17991799
)
18001800

18011801

1802+
def _artifact_key(ref: Any) -> tuple[str, str]:
1803+
"""产物的同一性:kind + 文件名(引用里只剩 basename,工作区相对路径不在 ArtifactRef 上)。"""
1804+
name = str(getattr(ref, "uri", "") or "").replace("\\", "/").rstrip("/").rsplit("/", 1)[-1]
1805+
return str(getattr(ref, "kind", "") or ""), name
1806+
1807+
18021808
class _SandboxCapture:
18031809
"""节点侧执行证据:最后一次 python_run 的 stdout/指标 + 全部产物 + 最终代码。
18041810
18051811
sandbox-run-report.v1 不含 stdout 与指标本体(那是产物与断言的事),但
18061812
节点输出(stage_outputs 正文)需要它们——在工具执行器上就地截获,不改
18071813
执行体的报告形状。``code`` 由 publish 回调顺手记下:执行体只回传产物 id,
18081814
节点要把脚本落到工作区固定路径还得有正文。
1815+
1816+
产物按 :func:`_artifact_key` 每个只留一份、位置按首次出现:沙盒新建和改写都采集,
1817+
同一个文件跑几次就报几版,留后一次运行的那版(工作区里就是它);同一次运行里
1818+
重名的取第一个。
18091819
"""
18101820

18111821
def __init__(self) -> None:
18121822
self.stdout = ""
18131823
self.metrics: dict[str, Any] = {}
18141824
self.artifacts: list[Any] = []
18151825
self.code = ""
1816-
self._seen: set[str] = set()
1826+
self._slots: dict[tuple[str, str], int] = {}
1827+
1828+
def add(self, ref: Any) -> None:
1829+
key = _artifact_key(ref)
1830+
slot = self._slots.get(key)
1831+
if slot is None:
1832+
self._slots[key] = len(self.artifacts)
1833+
self.artifacts.append(ref)
1834+
else:
1835+
self.artifacts[slot] = ref
18171836

18181837
def observe(self, result: ToolResult) -> None:
18191838
output = result.output or {}
@@ -1827,10 +1846,12 @@ def observe(self, result: ToolResult) -> None:
18271846
if isinstance(candidate, dict):
18281847
parsed = candidate
18291848
self.metrics = parsed
1849+
this_run: set[tuple[str, str]] = set()
18301850
for ref in result.artifacts:
1831-
if ref.artifact_id not in self._seen:
1832-
self._seen.add(ref.artifact_id)
1833-
self.artifacts.append(ref)
1851+
key = _artifact_key(ref)
1852+
if key not in this_run:
1853+
this_run.add(key)
1854+
self.add(ref)
18341855

18351856

18361857
def _sandbox_tool_executor(
@@ -1913,7 +1934,7 @@ def publish(code: str) -> str:
19131934
media_type,
19141935
ctx.step_id,
19151936
)
1916-
capture.artifacts.append(ref)
1937+
capture.add(ref)
19171938
return ref.artifact_id
19181939

19191940
return publish
@@ -2078,8 +2099,8 @@ def _execute_cleaning(
20782099
) -> tuple[dict[str, Any], list[Any]]:
20792100
"""清洗沙盒(子代理)→ 验收通过后进生成者-评审者环;返回 (cleaning, 产物引用)。
20802101
2081-
产物引用取所有波的并集:修复波只重写 cleaned/ 同名文件,沙盒按「新建文件」
2082-
捕获产物,首波的清洗数据引用不能因为修复而丢。
2102+
产物引用取所有波的并集(:func:`_union_artifacts`):修复波常只重写 cleaned/ 同名文件,
2103+
同名只留采用波那份,首波独有的清洗数据引用不能因为修复而丢。
20832104
"""
20842105
def skipped(reason: str) -> tuple[dict[str, Any], list[Any]]:
20852106
return {"executed": False, "reason": reason}, []
@@ -2419,21 +2440,28 @@ def _impact_stats_check(evidence) -> tuple[bool, str]:
24192440
def _union_artifacts(
24202441
waves: Sequence[_SandboxCapture], adopted: _SandboxCapture | None
24212442
) -> list[Any]:
2422-
"""所有波的产物引用并集(按出现顺序、uri 去重),未被采用那些波的脚本除外。
2443+
"""所有波的产物引用并集:同一产物(:func:`_artifact_key`)只留一份,位置按首次出现。
24232444
2424-
沙盒按「新建文件」捕获产物:修复波只重写同名文件时不会再报一次,首波产出的
2425-
数据 / 结果文件引用要留下来;脚本(kind=code)只留最终**采用**那一波的(僵持
2426-
时采用的可能不是最后一波)——其余版本在各波验收报告 ``final_code_artifact``
2427-
里仍可追溯。
2445+
修复波常重写首波的同名结果文件 / 图,沙盒每波都会再报一次:采用的那一波有就用它的
2446+
那版(和采用的指标对得上),没有就用最后一波的;只有首波产出过的数据 / 结果文件照样
2447+
留下。脚本(kind=code)只留最终**采用**那一波的(僵持时采用的可能不是最后一波)——
2448+
其余版本在各波验收报告 ``final_code_artifact`` 里仍可追溯。
24282449
"""
2429-
seen: set[str] = set()
24302450
merged: list[Any] = []
2451+
slots: dict[tuple[str, str], tuple[int, bool]] = {}
24312452
for capture in waves:
2453+
from_adopted = capture is adopted
24322454
for ref in capture.artifacts:
2433-
if ref.uri in seen or (ref.kind == "code" and capture is not adopted):
2455+
if ref.kind == "code" and not from_adopted:
24342456
continue
2435-
seen.add(ref.uri)
2436-
merged.append(ref)
2457+
key = _artifact_key(ref)
2458+
slot = slots.get(key)
2459+
if slot is None:
2460+
slots[key] = (len(merged), from_adopted)
2461+
merged.append(ref)
2462+
elif from_adopted or not slot[1]:
2463+
merged[slot[0]] = ref
2464+
slots[key] = (slot[0], from_adopted)
24372465
return merged
24382466

24392467

@@ -3389,7 +3417,7 @@ def stage_for_review(current: _SandboxCapture) -> None:
33893417
script_path = staged["path"]
33903418
else:
33913419
script_path = _stage_final_script(ctx, services, capture.code, language)
3392-
# 所有波的产物并集:修复波重写同名结果文件时沙盒不再报新建,首波引用要留
3420+
# 所有波的产物并集:同名只留采用波那份,首波独有的引用要留
33933421
artifacts = _union_artifacts(waves, capture)
33943422

33953423
return NodeResult.succeeded(
@@ -4740,8 +4768,8 @@ def _figures_rendered_check(
47404768
):
47414769
"""断言:规划的每张图都在工作区 ``directory`` 下、被采集为非空 figure 产物。
47424770
4743-
看的是本波节点侧累计采集(沙盒只采集每次运行**新建**的文件:重画同名文件不会
4744-
再次采集,所以修复波换 ``figures/revN/`` 目录、只看最后一次运行会误判)。
4771+
看的是本波节点侧累计采集:一波里可能分几次运行才画完,只看最后一次运行会漏掉
4772+
早先画好的图;盘上有文件但本波没新建或改写过它(上一波 / 上一轮留下的)不算。
47454773
"""
47464774

47474775
def check(evidence) -> tuple[bool, str]:
@@ -4755,7 +4783,7 @@ def check(evidence) -> tuple[bool, str]:
47554783
if f"{directory}{name}" not in files:
47564784
problems.append(f"{name} 未保存到 {directory}")
47574785
elif name not in captured:
4758-
problems.append(f"{name} 未被采集为非空图件产物(须由本次 python_run 新建、≥ {_PAPER_FIGURE_MIN_BYTES} 字节)")
4786+
problems.append(f"{name} 未被采集为非空图件产物(须由本波运行新建或改写、≥ {_PAPER_FIGURE_MIN_BYTES} 字节)")
47594787
if problems:
47604788
return False, ";".join(problems)
47614789
return True, f"{len(expected)} 张图已渲染并采集:{'、'.join(expected)}"
@@ -5291,8 +5319,8 @@ def skipped(reason: str) -> _PaperFigureResult:
52915319
def sandbox_wave(brief_suffix: str | None, max_runs: int) -> _SandboxWaveResult | None:
52925320
"""一次经监督者派发的补图沙盒波(首波 / 按审稿或静态检查意见修复)。
52935321
5294-
修复波换目录 ``figures/rev<N>/``:沙盒只采集**新建**文件,重画同名文件不会再次
5295-
采集,也就没法把修好的图当产物登记;文件名不变、目录带轮次,清单仍按 basename 记。
5322+
修复波换目录 ``figures/rev<N>/``:上一波的图原样留在盘上,僵持回落到上一波时盘上
5323+
文件与采用的产物仍是同一版;文件名不变、目录带轮次,清单仍按 basename 记。
52965324
"""
52975325
wave_no["count"] += 1
52985326
directory = _PAPER_FIGURE_DIR if wave_no["count"] == 1 else f"{_PAPER_FIGURE_DIR}rev{wave_no['count']}/"
@@ -5302,7 +5330,7 @@ def sandbox_wave(brief_suffix: str | None, max_runs: int) -> _SandboxWaveResult
53025330
if wave_no["count"] > 1:
53035331
brief += (
53045332
f"\n\n本波修复请把全部图保存到 `{directory}` 目录(文件名不变;"
5305-
"沙盒只采集新建文件,覆盖旧文件不算重新渲染)。"
5333+
"上一波的图不要覆盖)。"
53065334
)
53075335
if brief_suffix:
53085336
brief += "\n\n" + brief_suffix

‎agents/tools/src/omm_agent_tools/python_runner.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- a scrubbed environment (small Windows-safe allowlist; no user secrets),
77
- a hard wall-clock timeout with process kill,
88
- capped stdout/stderr capture,
9-
- automatic artifact capture of files the code creates in the workspace.
9+
- automatic artifact capture of files the code creates or rewrites in the workspace.
1010
1111
The subprocess runs with cwd at the WORKSPACE ROOT, not the per-step script
1212
directory: every other surface the model sees (ws_list/ws_read/ws_write,

‎agents/tools/src/omm_agent_tools/runners.py‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""多语言 Runner(设计 §7.4,H7 切片 1):统一 ``code_run`` 入口按 ``language`` 分发。
22
33
统一执行契约(§7.4,语言中立):脚本落到 ``steps/<step_id>/<main.ext>``、子进程
4-
cwd = 工作区根、环境白名单、超时杀进程、stdout / stderr 截断、新建文件按后缀
5-
归类采集为产物;指标协议同样语言中立——脚本自己打印 ``OMM_METRICS_JSON: {...}``
4+
cwd = 工作区根、环境白名单、超时杀进程、stdout / stderr 截断、新建或改写的文件
5+
按后缀归类采集为产物;指标协议同样语言中立——脚本自己打印 ``OMM_METRICS_JSON: {...}``
66
行 / 落 ``metrics.json``,执行核不解析(那是执行体与节点的事)。
77
88
每语言一个 :class:`LanguageSpec`(启动命令、脚本名、可执行文件候选、额外需要
@@ -622,6 +622,17 @@ def artifact_kind(name: str) -> str:
622622
return KIND_BY_SUFFIX.get(os.path.splitext(name)[1].lower(), "other")
623623

624624

625+
def _file_states(root: Path) -> dict[Path, tuple[int, int]]:
626+
"""``root`` 下每个文件的(大小, 修改时间 ns):运行前后各取一次,新出现或变了的就是本次产物。"""
627+
states: dict[Path, tuple[int, int]] = {}
628+
for path in root.rglob("*"):
629+
with contextlib.suppress(OSError):
630+
if path.is_file():
631+
stat = path.stat()
632+
states[path] = (stat.st_size, stat.st_mtime_ns)
633+
return states
634+
635+
625636
def clip_output(text: str, limit: int = OUTPUT_LIMIT) -> str:
626637
if len(text) > limit:
627638
return text[:limit] + f"\n...(+{len(text) - limit} chars truncated)"
@@ -634,7 +645,9 @@ class SubprocessRunner:
634645
与 python_run 的历史行为逐字节一致(PythonSandbox 现在只是它的薄壳):脚本写到
635646
``steps/<step_id>/<script_name>``、cwd = 工作区根(ws_* / 断言 / 下发数据都说
636647
工作区相对路径,生成代码里的相对路径必须落到同一个根)、快照在写脚本之后
637-
(脚本本身不算产物)、扫全工作区(代码可在根下任意位置建文件)。
648+
(脚本本身不算产物)、扫全工作区(代码可在根下任意位置建文件)。快照记每个文件的
649+
大小与修改时间,新建和改写都算本次产物:回退重做那一轮重写上一轮同名的图 / 结果表,
650+
只认新建就采集不到。
638651
"""
639652

640653
def __init__(
@@ -684,7 +697,7 @@ def run(
684697
)
685698

686699
scan_root = self._workspace.root
687-
before = {path for path in scan_root.rglob("*") if path.is_file()}
700+
before = _file_states(scan_root)
688701

689702
completed = run_process_tree(
690703
self.spec.command(executable, str(script_path)),
@@ -722,16 +735,14 @@ def run(
722735
return ToolResult(status="succeeded", output=output, artifacts=tuple(artifacts))
723736

724737
def _collect_artifacts(
725-
self, scan_root: Path, before: set[Path], ctx: ToolCallContext
738+
self, scan_root: Path, before: dict[Path, tuple[int, int]], ctx: ToolCallContext
726739
) -> tuple[list[ArtifactRef], list[str]]:
727740
artifacts: list[ArtifactRef] = []
728741
skipped: list[str] = []
729-
created = sorted(
730-
path
731-
for path in scan_root.rglob("*")
732-
if path.is_file() and path not in before
742+
changed = sorted(
743+
path for path, state in _file_states(scan_root).items() if before.get(path) != state
733744
)
734-
for path in created:
745+
for path in changed:
735746
if len(artifacts) >= MAX_ARTIFACTS:
736747
skipped.append(f"{path.name} (artifact limit {MAX_ARTIFACTS})")
737748
continue

0 commit comments

Comments
 (0)