mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-24 05:47:32 +00:00
* Add Docker benchmark sandbox and pooled runner * Fix benchmark worker reuse after Docker command timeout * Simplify benchmark worker requests and preserve artifacts * Harden benchmark sandbox reuse and isolate query state * Deduplicate benchmark sandbox cleanup and artifact checks * Report benchmark cleanup failures and preserve diagnostic artifacts * Handle optional Docker dependencies in benchmark unit tests * Keep benchmark Docker dependencies optional in CI * Fix cancellation of thread-pool background jobs * Finish background worker cleanup before propagating cancellation * Revert background worker cancellation changes * Fix benchmark worker plugin defaults before config validation * Stop interrupted benchmark containers before collecting diagnostics * Fix candidate exclusions and benchmark upload serialization * Fix late Docker creation cleanup and preserve query artifacts * Stream benchmark workspace archives and extract plan validation --------- Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
186 lines
6.1 KiB
Python
186 lines
6.1 KiB
Python
"""Candidate-code inputs shared by one or more isolated case sandboxes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import fnmatch
|
|
import gzip
|
|
import hashlib
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
from pathlib import PurePosixPath
|
|
import tarfile
|
|
|
|
DEFAULT_EXCLUDES = (
|
|
".git",
|
|
".reme",
|
|
".env",
|
|
".env.*",
|
|
".ssh",
|
|
".codex",
|
|
".claude",
|
|
".venv",
|
|
"venv",
|
|
"__pycache__",
|
|
".pytest_cache",
|
|
".mypy_cache",
|
|
".ruff_cache",
|
|
"datasets",
|
|
"benchmark",
|
|
"baselines",
|
|
"meta-reme",
|
|
"output",
|
|
"tests",
|
|
"cookbook",
|
|
"docs",
|
|
"github-pages",
|
|
"integrations",
|
|
"skills",
|
|
"reme_studio",
|
|
"node_modules",
|
|
"memory_workspaces",
|
|
"logs",
|
|
"artifacts",
|
|
"build",
|
|
"dist",
|
|
"*.egg-info",
|
|
"*.pyc",
|
|
"*.pyo",
|
|
"*.log",
|
|
)
|
|
|
|
|
|
def _is_excluded(relative_path: Path, patterns: tuple[str, ...]) -> bool:
|
|
"""Return whether any path component or full POSIX path is excluded."""
|
|
posix = relative_path.as_posix()
|
|
return any(
|
|
fnmatch.fnmatch(posix, pattern) or any(fnmatch.fnmatch(part, pattern) for part in relative_path.parts)
|
|
for pattern in patterns
|
|
)
|
|
|
|
|
|
def _normalized_info(path: Path, arcname: str) -> tarfile.TarInfo:
|
|
"""Create deterministic tar metadata for a regular source file."""
|
|
stat = path.stat()
|
|
info = tarfile.TarInfo(arcname)
|
|
info.size = stat.st_size
|
|
info.mode = stat.st_mode & 0o777
|
|
info.mtime = 0
|
|
info.uid = 0
|
|
info.gid = 0
|
|
info.uname = ""
|
|
info.gname = ""
|
|
return info
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourceSnapshot:
|
|
"""A deterministic source archive reusable across many case sandboxes."""
|
|
|
|
archive: bytes
|
|
sha256: str
|
|
file_count: int
|
|
|
|
@classmethod
|
|
def from_directory(
|
|
cls,
|
|
source_dir: str | Path,
|
|
*,
|
|
excludes: tuple[str, ...] = DEFAULT_EXCLUDES,
|
|
) -> "SourceSnapshot":
|
|
"""Snapshot a candidate directory without runtime or VCS state.
|
|
|
|
Symbolic links are rejected instead of followed. This prevents a
|
|
candidate archive from silently capturing files outside its root.
|
|
"""
|
|
root = Path(source_dir).resolve(strict=True)
|
|
if not root.is_dir():
|
|
raise ValueError(f"candidate source is not a directory: {root}")
|
|
|
|
files: list[tuple[Path, Path]] = []
|
|
# Component matches exclude every descendant. Full-path matches may
|
|
# exclude only the directory itself, so they must not prune traversal.
|
|
directory_patterns = tuple(pattern for pattern in excludes if "/" not in pattern)
|
|
for current, directories, filenames in os.walk(root, topdown=True, followlinks=False):
|
|
directory = Path(current)
|
|
for name in directories[:]:
|
|
path = directory / name
|
|
if any(fnmatch.fnmatch(name, pattern) for pattern in directory_patterns):
|
|
directories.remove(name)
|
|
elif path.is_symlink() and not _is_excluded(path.relative_to(root), excludes):
|
|
raise ValueError(f"candidate source contains a symbolic link: {path.relative_to(root)}")
|
|
for name in filenames:
|
|
path = directory / name
|
|
relative = path.relative_to(root)
|
|
if _is_excluded(relative, excludes):
|
|
continue
|
|
if path.is_symlink():
|
|
raise ValueError(f"candidate source contains a symbolic link: {relative}")
|
|
if path.is_file():
|
|
files.append((relative, path))
|
|
|
|
raw = io.BytesIO()
|
|
with gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) as compressed:
|
|
with tarfile.open(fileobj=compressed, mode="w") as archive:
|
|
for relative, path in sorted(files, key=lambda item: item[0].as_posix()):
|
|
info = _normalized_info(path, relative.as_posix())
|
|
with path.open("rb") as source:
|
|
archive.addfile(info, source)
|
|
|
|
payload = raw.getvalue()
|
|
return cls(
|
|
archive=payload,
|
|
sha256=hashlib.sha256(payload).hexdigest(),
|
|
file_count=len(files),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourceCandidate:
|
|
"""A changing source snapshot containing one or more Python projects.
|
|
|
|
``install_projects`` is a trusted, archive-relative installation plan. A
|
|
plugin directory merely being present in the snapshot is insufficient:
|
|
installing it is what publishes its ``reme.plugins`` entry point.
|
|
"""
|
|
|
|
snapshot: SourceSnapshot
|
|
base_image: str = "reme-benchmark-base:agentscope-2.0.9"
|
|
install_projects: tuple[str, ...] = (".",)
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.install_projects:
|
|
raise ValueError("source candidate requires at least one install project")
|
|
normalized: list[str] = []
|
|
for value in self.install_projects:
|
|
if not isinstance(value, str) or not value:
|
|
raise TypeError("candidate install projects must be non-empty strings")
|
|
path = PurePosixPath(value)
|
|
if path.is_absolute() or ".." in path.parts or "\\" in value:
|
|
raise ValueError(f"candidate install project must stay inside the archive: {value!r}")
|
|
name = path.as_posix()
|
|
if name in normalized:
|
|
raise ValueError(f"candidate install project is duplicated: {value!r}")
|
|
normalized.append(name)
|
|
if normalized[0] != ".":
|
|
raise ValueError("the candidate root project must be installed before plugin projects")
|
|
object.__setattr__(self, "install_projects", tuple(normalized))
|
|
|
|
@property
|
|
def candidate_id(self) -> str:
|
|
"""Stable identifier for the exact source snapshot."""
|
|
return self.snapshot.sha256
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ImageCandidate:
|
|
"""A ReMe candidate already installed in an immutable Docker image."""
|
|
|
|
image: str
|
|
candidate_id: str | None = None
|
|
|
|
@property
|
|
def resolved_candidate_id(self) -> str:
|
|
"""Return the caller-supplied digest/ID or the image reference."""
|
|
return self.candidate_id or self.image
|