Files
hermes-agent/evals/memory/honcho_current_query.py
teknium1 956732d4a8 chore(evals,desktop): drop literal /tmp from eval harnesses and desktop scripts
Eval probes wrote fixtures, receipts and evidence to hard-coded /tmp paths and
two live A/B tasks literally instructed the model to work in /tmp. They now
derive locations from tempfile.gettempdir() / os.tmpdir() (env overrides kept),
usage examples use relative output names, and the desktop e2e screenshot dirs,
perf scripts and the short-session repro fixture stop naming /tmp. Also adds
the explicit encoding= the windows-footgun check wants in the touched files.
2026-09-19 10:44:26 -07:00

204 lines
8.6 KiB
Python

"""Honcho 2.2 SDK/local HTTP lifecycle probe; no hosted service or model inference.
Run with isolated HOME/HERMES_HOME and honcho-ai==2.2.0 installed (or on PYTHONPATH):
.venv/bin/python evals/memory/honcho_current_query.py --out honcho-proof.json
Prepared ongoing sessions bypass startup/migration. Message writes are disabled.
The fixture proves query routing, ownership and caller waiting, not memory quality.
"""
import argparse
import json
import socket
import sys
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
from urllib.parse import parse_qs, urlparse
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
args = argparse.ArgumentParser()
args.add_argument("--out", type=Path, required=True)
args = args.parse_args()
events = []
release = threading.Event()
entered = threading.Event()
invalidate = None
delay = 0
class Backend(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def handle_request(self):
parsed = urlparse(self.path)
query = parse_qs(parsed.query)
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}")
search = query.get("search_query", [""])[0]
events.append({"path": parsed.path, "search": search, "body": body})
if delay:
time.sleep(delay)
if search.startswith("BLOCK"):
entered.set()
release.wait(10)
if search.startswith("GENERATION") and invalidate:
invalidate()
status = 200
if parsed.path.endswith("/context"):
if search.startswith("ERROR"):
status, data = 400, {"error": "fixture rejection"}
else:
data = {"peer_id": "assistant", "target_id": "user",
"representation": "RETRIEVED_FOR=" + search if search else "AI identity",
"peer_card": ["fixture card"]}
if search.startswith("EMPTY") or empty_all:
data.update(representation="", peer_card=[])
elif parsed.path.endswith("/chat"):
data = {"content": "" if empty_all else "DIALECTIC_FOR=" + body.get("query", "")}
elif parsed.path.endswith("/peers"):
data = {"id": body["id"], "workspace_id": "probe", "created_at": "2026-01-01T00:00:00Z",
"metadata": {}, "configuration": {}}
elif parsed.path.endswith("/workspaces"):
data = {"id": "probe", "metadata": {}, "configuration": {}}
else:
status, data = 404, {"error": "no fixture fallback"}
raw = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
do_GET = handle_request
do_POST = handle_request
empty_all = False
server = ThreadingHTTPServer(("127.0.0.1", 0), Backend)
threading.Thread(target=server.serve_forever, daemon=True).start()
original_connect = socket.socket.connect
def fixture_only(self, address):
if not isinstance(address, tuple) or address[:2] != ("127.0.0.1", server.server_port):
raise RuntimeError(f"Blocked nonfixture connection: {address}")
return original_connect(self, address)
socket.socket.connect = fixture_only
from plugins.memory.honcho import HonchoMemoryProvider # noqa: E402
from plugins.memory.honcho.client import HonchoClientConfig # noqa: E402
from plugins.memory.honcho.session import HonchoSession, HonchoSessionManager # noqa: E402
from agent.memory_manager import MemoryManager # noqa: E402
from agent.turn_context import _memory_turn_start_and_prefetch, compose_user_api_content # noqa: E402
from run_agent import AIAgent # noqa: E402
def prepared(sync):
p = HonchoMemoryProvider()
with tempfile.TemporaryDirectory() as home:
p.save_config({"baseUrl": f"http://127.0.0.1:{server.server_port}", "workspace": "probe",
"timeout": 2, "contextTokens": 1200, "saveMessages": False,
"queryRewrite": False, "recallSync": True,
"hosts": {"hermes": {"recallSync": sync}}}, home)
cfg = HonchoClientConfig.from_global_config(host="hermes", config_path=Path(home) / "honcho.json")
assert cfg.recall_sync is sync
manager = HonchoSessionManager(config=cfg, context_tokens=1200)
manager._cache["ongoing"] = HonchoSession("ongoing", "user", "assistant", "ongoing")
p._config, p._manager, p._session_key, p._session_initialized = cfg, manager, "ongoing", True
p._recall_sync = cfg.recall_sync
p._dialectic_cadence = 100
p._last_dialectic_turn = 0
mm = MemoryManager(external_prefetch_timeout=3)
mm.add_provider(p)
a = SimpleNamespace(_memory_manager=mm, session_id="ongoing", _user_turn_count=1)
return p, a
def begin(a, turn, query):
a._user_turn_count = turn
started = time.monotonic()
context = _memory_turn_start_and_prefetch(a, query)
return {"query": query, "context": context, "elapsed": time.monotonic() - started,
"api_content": compose_user_api_content(query, context, "")}
def end(a, query):
AIAgent._sync_external_memory_for_turn(a, original_user_message=query,
final_response="Fixture answer; no LLM called.", interrupted=False)
def settle(p):
for thread in (getattr(p, "_recall_sync_thread", None), p._prefetch_thread):
if thread:
thread.join(5)
assert not thread.is_alive()
legacy, a = prepared(False)
first = begin(a, 1, "What is my preferred telescope?")
proof = {"boundary": __doc__, "sdk": sys.modules["honcho"].__file__,
"provider": sys.modules[HonchoMemoryProvider.__module__].__file__}
end(a, first["query"])
deadline = time.monotonic() + 5
while not legacy._manager._context_cache and time.monotonic() < deadline:
time.sleep(0.01)
second = begin(a, 2, "Which sourdough starter do I use?")
assert first["query"] in second["context"] and second["query"] not in second["context"]
proof["default_previous_query"] = second
p, a = prepared(True)
p._dialectic_cadence = 1
rows = []
for turn, query in enumerate(("What is my preferred telescope?", "Which sourdough starter do I use?"), 1):
row = begin(a, turn, query)
assert "RETRIEVED_FOR=" + query in row["context"]
if rows:
assert rows[-1]["query"] not in row["context"]
before = len(events)
end(a, query)
assert len(events) == before, "sync mode must not queue duplicate automatic retrieval"
rows.append(row)
proof["current_queries"] = rows
assert all(row["query"] in row["context"].split("DIALECTIC_FOR=")[1] for row in rows)
p._config.timeout = 0.05
blocked = begin(a, 3, "BLOCK Which hiking boots do I wear?")
assert entered.wait(2) and blocked["context"] == "" and blocked["elapsed"] < 2
before = len(events)
for turn in range(4, 9):
assert begin(a, turn, "Which fountain pen ink do I use?")["context"] == ""
assert len(events) == before, "blocked worker must retain its single-flight slot"
release.set()
settle(p)
p._config.timeout = 2
recovered = begin(a, 9, "Which violin strings do I prefer?")
assert recovered["query"] in recovered["context"] and "BLOCK" not in recovered["context"]
proof["timeout_and_recovery"] = {"blocked": blocked, "recovered": recovered, "busy_calls": 5}
delay = 0.04
p._config.timeout = 0.06
responsive = begin(a, 10, "Which garden plants do I grow?")
assert responsive["context"] == "" and responsive["elapsed"] < 2
settle(p)
delay = 0
p._config.timeout = 2
proof["shared_deadline"] = responsive
invalidate = lambda: p.on_turn_start(10, "Different request with the same turn number")
assert begin(a, 10, "GENERATION What watercolor paints do I prefer?")["context"] == ""
assert begin(a, 11, "ERROR Which watercolor paints do I prefer?")["context"] == ""
empty_all = True
assert begin(a, 12, "EMPTY Which watercolor paints do I prefer?")["context"] == ""
empty_all = False
p._context_cadence = 3
assert begin(a, 13, "Which aquarium food do I buy?")["context"] == ""
proof["checks"] = {"alignment": True, "default_unchanged": True, "single_flight": True,
"late_discarded": True, "generation_discarded": True, "error_omitted": True,
"empty_omitted": True, "cadence_gap_empty": True, "no_duplicate_end_turn": True}
proof["events"] = events
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(proof, indent=2), encoding="utf-8")
print(json.dumps({"checks": proof["checks"], "out": str(args.out),
"current_elapsed": [row["elapsed"] for row in rows],
"timeout_elapsed": blocked["elapsed"]}, indent=2))
server.shutdown()