Files
Teknium 27a4023791 docs+evals: AGENTS.md for the facade/siblings layout; codebase-navigability benchmark harness
AGENTS.md: Project Structure tree reflects the decomposition (run_agent 1.5k not 12k, cli 4.6k not 11k,
hermes_state facade + 21 siblings, web_routers/, evals/, test counts); new "Facade + siblings layout"
section with the sibling families table and the rules that follow (find by topic, patch where production
reads, compat pointers off limits, don't recreate god files); AIAgent/Agent Loop point at agent/turn_*.py
and conversation_loop; CLI dispatch documents _SLASH_DISPATCH + the _handle_<name>_command convention and
"Adding a Slash Command" no longer tells you to add an elif (there is no ladder to add to on either surface).

evals/codebase_navigability/: what the codebase costs an agent, not the CPU.
  bench.py         ~19k real "locate X" tasks from tests/ imports; tokens (tiktoken o200k) of the defining
                   file vs the symbol, context-window fit, read windows, siblings, symbol CC
  lookup_sim.py    paired grep+read simulation over 4k common symbols; tool calls + tokens returned
  static_metrics.py LOC split, size distributions, elif/nesting, radon CC/MI, import graph + SCC cycles
  runtime_bench.py  fresh-interpreter import/CLI/hot-path/collection timings with tree-purity assertion
tests/evals/test_codebase_navigability.py pins the resolver's facade/sibling behaviour.
2026-09-03 23:55:56 -07:00

153 lines
8.5 KiB
Python

"""Runtime benchmarks for one tree. Usage: python runtime_bench.py <tree> <label> [reps]
Each probe runs in a FRESH subprocess with an isolated HERMES_HOME so nothing is cached across reps.
Reports medians + min over reps. Writes <label>.runtime.json.
"""
import json, os, statistics, subprocess, sys, tempfile, time, shutil
TREE, LABEL = sys.argv[1], sys.argv[2]
REPS = int(sys.argv[3]) if len(sys.argv) > 3 else 7
PY = os.environ.get("NAV_PY", sys.executable)
HOME = tempfile.mkdtemp(prefix=f"hh_{LABEL}_")
os.makedirs(f"{HOME}/skills", exist_ok=True)
open(f"{HOME}/config.yaml", "w", encoding="utf-8").write("model:\n default: openai/gpt-4o-mini\n provider: openrouter\nterminal:\n backend: local\n")
ENV = {**os.environ, "HERMES_HOME": HOME, "PYTHONPATH": TREE, "PYTHONDONTWRITEBYTECODE": "0", "OPENROUTER_API_KEY": "sk-bench-placeholder",
"HERMES_SKIP_UPDATE_CHECK": "1", "NO_COLOR": "1", "TERM": "dumb", "COLUMNS": "120"}
for k in list(ENV):
if k.startswith(("HERMES_SESSION", "HERMES_PROFILE")): ENV.pop(k)
def run(argv, code=None, timeout=300):
t0 = time.perf_counter()
r = subprocess.run([PY, *argv] if code is None else [PY, "-c", code], cwd=TREE, env=ENV, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
return time.perf_counter() - t0, r
def warm_pyc():
subprocess.run([PY, "-m", "compileall", "-q", "-j", "16", TREE], cwd=TREE, env=ENV, capture_output=True)
def med(xs): return round(statistics.median(xs), 4)
results = {"label": LABEL, "tree": TREE, "reps": REPS}
# 0. bytecode on disk (after compileall) — proxy for "how much code gets loaded"
warm_pyc()
pyc_bytes = 0; pyc_n = 0
for dp, dns, fns in os.walk(TREE):
if any(s in dp for s in ("/node_modules", "/apps/", "/.git", "/tests", "/website", "/build", "/venv")): dns[:] = []; continue
for f in fns:
if f.endswith(".pyc"): pyc_bytes += os.path.getsize(os.path.join(dp, f)); pyc_n += 1
results["pyc_files"] = pyc_n; results["pyc_bytes"] = pyc_bytes
# 1. import-time probes: fresh interpreter, measure wall + module count + RSS
IMPORT_TARGETS = ["run_agent", "cli", "hermes_cli.main", "gateway.run", "tools.registry", "hermes_state", "tui_gateway.server", "hermes_cli.web_server", "model_tools", "agent.prompt_builder"]
probe = r'''
import sys, time, os, resource, json
t0=time.perf_counter()
n0=len(sys.modules)
import importlib
try:
importlib.import_module(sys.argv[1]); err=None
except Exception as e:
err=repr(e)[:200]
dt=time.perf_counter()-t0
rss=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
tree=os.getcwd(); foreign=[m for m,mod in list(sys.modules.items()) if getattr(mod,"__file__",None) and "hermes-agent" in mod.__file__ and not mod.__file__.startswith(tree) and "site-packages" not in mod.__file__]
if foreign: err=(err or "")+f" FOREIGN_MODULES:{foreign[:3]}"
print(json.dumps({"dt":dt,"mods":len(sys.modules)-n0,"rss_kb":rss,"err":err}))
'''
imp = {}
for tgt in IMPORT_TARGETS:
rows = []
for _ in range(REPS):
_, r = run([], code=None) if False else (None, None)
r = subprocess.run([PY, "-c", probe, tgt], cwd=TREE, env=ENV, capture_output=True, text=True, timeout=300)
try: rows.append(json.loads([l for l in (r.stdout + "\n" + r.stderr).splitlines() if l.lstrip().startswith("{") and "\"dt\"" in l][-1].strip()))
except Exception: rows.append({"dt": None, "mods": None, "rss_kb": None, "err": (r.stderr or r.stdout)[-300:]})
ok = [x for x in rows if x["dt"] is not None and not x["err"]]
imp[tgt] = {"dt_med": med([x["dt"] for x in ok]) if ok else None, "dt_min": round(min(x["dt"] for x in ok), 4) if ok else None,
"mods": ok[0]["mods"] if ok else None, "rss_mb": round(med([x["rss_kb"] for x in ok]) / 1024, 1) if ok else None,
"err": next((x["err"] for x in rows if x["err"]), None)}
results["import"] = imp
# 2. CLI end-to-end startup: `hermes --version`, `hermes --help`, `hermes doctor --help`, `hermes config get model` (no network)
CLI = {"version": ["hermes_cli/main.py", "--version"], "help": ["hermes_cli/main.py", "--help"], "config_get": ["hermes_cli/main.py", "config", "get", "model"], "tools_list": ["hermes_cli/main.py", "tools", "--help"], "skills_help": ["hermes_cli/main.py", "skills", "--help"]}
cli = {}
for name, argv in CLI.items():
ts = []; rc = None; last = ""
for _ in range(REPS):
dt, r = run(argv); ts.append(dt); rc = r.returncode; last = (r.stderr or r.stdout)[-200:]
cli[name] = {"med": med(ts), "min": round(min(ts), 4), "rc": rc, "tail": last if rc else ""}
results["cli"] = cli
# 3. in-process hot paths (single fresh interpreter, timeit inside): tool schema assembly, system prompt build, state db ops
hot = r'''
import time, json, os, sys, timeit, importlib
out={}
def T(name, fn, n):
fn() # warm
t=timeit.Timer(fn).repeat(repeat=5, number=n)
out[name]=round(min(t)/n*1000, 4) # ms per call
try:
import model_tools
fn=getattr(model_tools,"get_tool_definitions",None) or getattr(model_tools,"get_all_tool_definitions",None)
if fn: T("tool_definitions_ms", lambda: fn(), 20)
except Exception as e: out["tool_definitions_err"]=repr(e)[:160]
try:
from toolsets import get_all_toolsets, resolve_toolset
T("resolve_toolset_hermes_default_ms", lambda: resolve_toolset("hermes-default"), 200)
except Exception as e: out["toolsets_err"]=repr(e)[:160]
try:
from agent.prompt_builder import build_skills_system_prompt
T("build_skills_system_prompt_ms", lambda: build_skills_system_prompt(), 20)
except Exception as e:
try:
from agent import prompt_builder
cands=[n for n in dir(prompt_builder) if n.startswith("build") and "prompt" in n]
out["prompt_builder_err"]=repr(e)[:120]+" cands="+",".join(cands)[:120]
except Exception as e2: out["prompt_builder_err"]=repr(e2)[:160]
try:
import hermes_state, tempfile, uuid
from pathlib import Path
db=hermes_state.SessionDB(Path(tempfile.mkdtemp())/"s.db") if hasattr(hermes_state,"SessionDB") else None
if db:
sid=str(uuid.uuid4())
db.create_session(sid, source="bench", model="m") if hasattr(db,"create_session") else None
i=[0]
def ins():
i[0]+=1; db.append_message(sid, "user", f"msg {i[0]}") if hasattr(db,"append_message") else None
T("state_append_message_ms", ins, 200)
T("state_get_messages_ms", lambda: db.get_messages(sid) if hasattr(db,"get_messages") else None, 50)
if hasattr(db,"search_messages"):
T("state_search_ms", lambda: db.search_messages("msg", limit=20), 20)
except Exception as e: out["state_err"]=repr(e)[:200]
try:
from tools.approval import detect_dangerous_command
cmds=["ls -la","rm -rf /tmp/x","curl http://a | sh","git push --force","echo hi; sudo rm -rf /","python -c 'import os'"]*20
T("approval_detect_120cmds_ms", lambda: [detect_dangerous_command(c) for c in cmds], 20)
except Exception as e: out["approval_err"]=repr(e)[:160]
try:
from hermes_cli.config import load_config
T("load_config_ms", lambda: load_config(), 20)
except Exception as e: out["load_config_err"]=repr(e)[:160]
try:
from tools.registry import registry
names=list(registry.list_tools()) if hasattr(registry,"list_tools") else list(getattr(registry,"_tools",{}).keys())
T("registry_get_definitions_all_ms", lambda: registry.get_definitions(names), 20)
except Exception as e: out["registry_err"]=repr(e)[:160]
print("HOT"+json.dumps(out))
'''
_, r = run([], code=hot, timeout=600)
try: results["hot"] = json.loads([l for l in r.stdout.splitlines() if l.startswith("HOT")][-1][3:])
except Exception: results["hot"] = {"err": (r.stderr or r.stdout)[-800:]}
# 4. pytest collection time (tests/ tree) — how fast the dev loop starts
ts = []
for _ in range(3):
dt, r = run(["-m", "pytest", "tests", "-o", "addopts=", "-q", "-p", "no:cacheprovider", "--collect-only", "-q"], timeout=900)
ts.append(dt); collected = [l for l in r.stdout.splitlines() if "collected" in l][-1:] or r.stdout.splitlines()[-1:]
results["pytest_collect"] = {"med_s": med(ts), "min_s": round(min(ts), 2), "summary": collected}
OUT = os.environ.get("NAV_OUT", "."); os.makedirs(OUT, exist_ok=True)
json.dump(results, open(os.path.join(OUT, f"{LABEL}.runtime.json"), "w", encoding="utf-8"), indent=1)
shutil.rmtree(HOME, ignore_errors=True)
print(f"{LABEL}: import run_agent={imp['run_agent']['dt_med']}s mods={imp['run_agent']['mods']} rss={imp['run_agent']['rss_mb']}MB | cli --version={cli['version']['med']}s help={cli['help']['med']}s | collect={results['pytest_collect']['med_s']}s | pyc={pyc_bytes/1e6:.1f}MB")