82 lines
2.4 KiB
Python

"""不可变 run 目录读写。不认识 Muse 业务对象。"""
from __future__ import annotations
import json
import os
import re
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from pathlib import Path
from typing import Any, BinaryIO
DEFAULT_RUN_DIR_ROOT = Path("/tmp/muse-agent-runs")
_RUN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
class RunDirectoryError(OSError):
"""run 目录创建或写入失败。"""
def validate_run_id(run_id: str) -> bool:
return isinstance(run_id, str) and _RUN_ID_PATTERN.fullmatch(run_id) is not None
def prepare_run_dir(run_id: str, run_dir: str | Path | None = None) -> Path:
"""创建仅当前用户可访问的 run 目录;默认根目录可复用,本次目录不可覆盖。"""
run_dir_path = Path(run_dir) if run_dir is not None else DEFAULT_RUN_DIR_ROOT / run_id
if run_dir is None:
DEFAULT_RUN_DIR_ROOT.mkdir(parents=True, mode=0o700, exist_ok=True)
DEFAULT_RUN_DIR_ROOT.chmod(0o700)
run_dir_path.mkdir(parents=True, mode=0o700, exist_ok=False)
run_dir_path.chmod(0o700)
return run_dir_path
def write_private_text(path: Path, text: str) -> None:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(text)
path.chmod(0o600)
def write_private_json(path: Path, value: Mapping[str, Any]) -> None:
write_private_text(path, json.dumps(value, ensure_ascii=False, indent=2))
def seed_run_inputs(
run_dir: Path,
*,
spec_text: str,
system_prompt: str,
user_message: str,
) -> None:
write_private_text(run_dir / "task-spec.json", spec_text)
write_private_text(run_dir / "system-prompt.txt", system_prompt)
write_private_text(run_dir / "user-message.txt", user_message)
@contextmanager
def open_transcript(run_dir: Path) -> Iterator[BinaryIO]:
path = run_dir / "transcript.jsonl"
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
handle = os.fdopen(fd, "wb")
try:
yield handle
path.chmod(0o600)
finally:
handle.close()
def read_transcript_text(run_dir: Path) -> str:
return (run_dir / "transcript.jsonl").read_text(encoding="utf-8")
def remove_local_raw(run_dir: Path) -> None:
for name in ("transcript.jsonl", "final-message.txt"):
try:
(run_dir / name).unlink(missing_ok=True)
except OSError:
pass