ReMe/benchmark/compaction/hermes_worker.py
jinliyl 548b08561e
refactor: generalize decision models and add recoverable context compaction (#626)
* refactor: generalize decision models and add opt-in context compaction

* chore: remove compaction-specific license metadata

* test: add live decision compaction scenarios and strategy guides

* docs: describe current decision model configuration directly

* refactor: preserve recoverable tool evidence and add compaction benchmarks

* docs: visualize compaction benchmark results with English charts

* docs: fix compaction benchmark links with verified permalinks

* benchmark: evaluate fixed-summary evidence additions and summary-aware selection

* docs: report summary-aware incremental evidence results

* docs: unify compaction strategy comparison

* refactor: replace compaction studies with one matched-input benchmark

* docs: pin matched-input results links

* bench: evaluate gold-labelled HotpotQA trajectories collected with Hermes

* docs: pin gold-labelled benchmark results

* bench: focus on frozen-summary Jev Top-N payment evaluation

* docs: pin frozen-summary benchmark results

* bench: evaluate fixed-summary fusion across Hermes fixture tasks

* docs: pin compaction study references to reviewed results

* bench: compare Jev fusion across frozen summary strategies

* docs: pin dual-summary benchmark and figure references

* docs: specify reproducible compaction settings and Hermes summary scope

* docs: plot Jev token overhead by retained Top N

* docs: pin reproducibility settings and revised cost figure

* docs: price total compaction and answering cost across Top N

* docs: pin final cost accounting and reproduction references

* docs: explain compaction experiment background setup and conclusions inline

* docs: compare compaction-only costs and omit Full from cost panel

* docs: pin compaction-only cost references

* docs: retain only quality and context-length experiment comparisons

* docs: pin streamlined experiment references

* docs: add Chinese counterparts for all compaction benchmark guides

* docs: link benchmark guides in the matching language

* docs: consolidate bilingual compaction benchmark guides

* docs: point compaction references to consolidated benchmark

* docs: reference Hermes Jev compaction evaluation results

* fix(benchmark): validate cached native summary provenance
2026-10-19 17:56:30 +08:00

197 lines
8.1 KiB
Python

"""Collect one genuine Hermes trajectory in an isolated process and read-only fixture workspace."""
import argparse
import hashlib
import importlib
import json
import os
from pathlib import Path
import re
import sys
import time
from dotenv import dotenv_values
from benchmark.compaction.cases import write
def collect(root, hermes, env_file):
"""Configure isolated Hermes state before importing its runtime; never save credentials."""
values = dotenv_values(env_file)
secrets = [values[k] for k in ("LLM_API_KEY", "DECISION_MODEL_API_KEY") if values.get(k)]
task = json.loads((root / "collection-task.json").read_text())
work, home = root / "workspace", root / "hermes_home"
home.mkdir()
os.environ.update(
{
"HERMES_HOME": str(home),
"TERMINAL_ENV": "local",
"TERMINAL_CWD": str(work),
"TOKENIZERS_PARALLELISM": "false",
},
)
(home / "config.yaml").write_text(
"compression:\n enabled: false\n micro_compact_enabled: false\n"
f"terminal:\n backend: local\n cwd: {work}\n"
"model:\n provider: custom\n context_length: 131072\n",
)
sys.path.insert(0, str(hermes))
from run_agent import AIAgent
from hermes_state import SessionDB
requests = []
def append(name, data):
text = json.dumps(data, ensure_ascii=False)
if any(secret in text for secret in secrets):
raise ValueError("Credential in captured body")
with (root / name).open("a", encoding="utf-8") as target:
target.write(text + "\n")
seen = set()
for name in ("httpx", "httpx2"):
try:
module = importlib.import_module(name)
except ImportError:
continue
if id(module.Client) in seen:
continue
seen.add(id(module.Client))
original = module.Client.send
def wrap_send(send):
def capture(client, request, *args, **kwargs):
captured = "/chat/completions" in request.url.path
if captured:
body = json.loads(request.content)
requests.append(body)
append("api_requests.jsonl", body)
response = send(client, request, *args, **kwargs)
if captured:
response.read()
try:
body = response.json()
except ValueError:
body = response.text
append("api_responses.jsonl", {"status": response.status_code, "body": body})
return response
return capture
module.Client.send = wrap_send(original)
agent = None
database = SessionDB(home / "state.db")
started = time.monotonic()
try:
agent = AIAgent(
base_url=values["LLM_BASE_URL"],
api_key=values["LLM_API_KEY"],
provider="custom",
api_mode="chat_completions",
model=task["model"],
max_iterations=12,
run_budget_seconds=240,
max_tokens=1024,
request_overrides={"reasoning_effort": "none", "extra_body": {"enable_thinking": False}},
enabled_toolsets=["file"],
save_trajectories=False,
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
skip_background_review=True,
session_db=database,
cwd=str(work),
platform="batch",
)
from tools.registry import registry
entry = registry.get_entry("read_file")
original_read = entry.handler
def confined_read(args, **kwargs):
path = Path(args.get("path", ""))
resolved = (path if path.is_absolute() else work / path).resolve()
if not resolved.is_relative_to(work) or str(resolved.relative_to(work)) not in task["source_hashes"]:
return json.dumps({"error": "Only the listed source snapshots may be read"})
return original_read(args, **kwargs)
entry.handler = confined_read
agent.tools = [t for t in agent.tools if t["function"]["name"] == "read_file"]
agent.valid_tool_names = {"read_file"}
agent.context_compressor.enabled = False
result = agent.run_conversation(
task["prompt"],
task_id=root.name,
system_message=f"Investigate the supplied fixture evidence and propose actions only. "
f"Use only read_file within {work}. "
"Do not read environment files, credentials or any path outside this workspace.",
)
if not result.get("completed") or not requests:
raise ValueError("Hermes collection did not complete")
systems = [m for m in requests[0]["messages"] if m["role"] == "system"]
history = [*systems, *result["messages"]]
if len(systems) != 1 or history[-1]["role"] != "assistant" or history[-1].get("tool_calls"):
raise ValueError("Incomplete system prompt or final assistant report")
calls = {c["id"]: c for m in history for c in m.get("tool_calls", []) or []}
results = [m for m in history if m["role"] == "tool"]
if set(calls) != {m["tool_call_id"] for m in results}:
raise ValueError("Unpaired tool calls")
observed = set()
for message in results:
call = calls[message["tool_call_id"]]
args = json.loads(call["function"]["arguments"])
path = Path(args.get("path") or args.get("file_path"))
path = (path if path.is_absolute() else work / path).resolve()
output = json.loads(message["content"])
if not path.is_relative_to(work) or str(path.relative_to(work)) not in task["source_hashes"]:
if "error" not in output:
raise ValueError("Unexpected content from outside the source allowlist")
continue # Denied reads remain in the trajectory; they never reached the filesystem.
text = "\n".join(re.sub(r"^\d+\|", "", line) for line in output.get("content", "").splitlines())
if text.rstrip() == path.read_text(encoding="utf-8").rstrip():
observed.add(str(path.relative_to(work)))
if observed != set(task["source_hashes"]):
raise ValueError("Not every source snapshot was read without truncation")
if any(
hashlib.sha256((work / name).read_bytes()).hexdigest() != sha for name, sha in task["source_hashes"].items()
):
raise ValueError("Source fixture changed")
write(root / "trajectory.full.json", {"messages": history})
write(root / "result.json", result)
write(root / "session_export.json", database.export_session(agent.session_id, include_compacted=True))
write(
root / "collection.json",
{
"completed": True,
"session_id": agent.session_id,
"messages": len(history),
"tool_calls": len(calls),
"articles_read": len(observed),
"source_unchanged": True,
"seconds": time.monotonic() - started,
"trajectory_sha256": hashlib.sha256((root / "trajectory.full.json").read_bytes()).hexdigest(),
},
)
if any(
secret.encode() in file.read_bytes() for file in root.rglob("*") if file.is_file() for secret in secrets
):
raise ValueError("Credential in artifacts")
print(root.name, "collected", len(calls), "tool calls", flush=True)
finally:
if agent is not None:
agent.close()
database.close()
def main():
"""Internal subprocess entry point; provider authorization belongs to the parent collector."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--hermes", type=Path, required=True)
parser.add_argument("--env-file", type=Path, required=True)
args = parser.parse_args()
collect(args.root.resolve(), args.hermes.resolve(), args.env_file.resolve())
if __name__ == "__main__":
main()