"""Emergent stress test — random word triangulation against the substrate. Pick three random words from /usr/share/dict/words, ask Hermes at temp 0.8 to weave them into a creative question paragraph, send that paragraph to arborist, log the whole journey (words → question → answer → audit_mode → sources → timings) as a single JSONL line. Designed for **blue moon cadence**, NOT every-commit benching. The combinatoric word space surfaces failure modes the curated bench (`bench/qa_questions.txt`) doesn't reach: question shapes the authors didn't anticipate, vocabulary the corpus barely covers, adversarial premises that emerge by accident. The teacher review step is intentionally **not** automated here. Fox brings interesting log entries to a teacher model (Claude Opus 4.7 in the current setup) and asks for guidance: - did the answer match the question? (mismatch → bench-max signal) - novelty class: known_truth_grounding / emergent_synthesis / novel_claim / no_signal (accounting for the 2010-11 Wikipedia corpus legitimately lacking post-2010 advances in science / math / engineering) - which hyperparam to tune for the next iteration Future: `--generator-endpoint` & `--student-endpoint` to swap upstreams (different model per role), and a separate review-side script that prompts a teacher model via API. Usage: make bench-emergent # 10 cycles, default make bench-emergent N=50 SEED=42 # bigger sample, reproducible python scripts/bench_emergent.py --n 10 --seed 42 python scripts/bench_emergent.py --print-pending # show un-reviewed entries """ from __future__ import annotations import argparse import json import os import random import re import sys import time from pathlib import Path # All paths default to the repo's bench/ tree so re-running across # branches accumulates a continuous log of every emergent cycle. DEFAULT_WORDS_PATH = Path("/usr/share/dict/words") DEFAULT_LOG_PATH = Path("bench/emergent_log.jsonl") # Word filter: skip too-short / proper-noun / abbreviation / weird # punctuation. The goal is "common English content tokens" that # Hermes can actually weave into a coherent paragraph. _WORD_RE = re.compile(r"^[a-z]{5,12}$") def pick_words( n: int = 3, *, words_path: Path = DEFAULT_WORDS_PATH, rng: random.Random | None = None, ) -> list[str]: """Sample N random words satisfying the content-token filter.""" rng = rng or random.Random() pool = [ w for w in words_path.read_text().splitlines() if _WORD_RE.fullmatch(w.strip().lower()) ] if len(pool) < n: raise RuntimeError( f"word pool too small ({len(pool)} usable words at " f"{words_path}); expected ≥{n}" ) return rng.sample(pool, n) GENERATOR_PROMPT = """\ Write a single short paragraph (2-3 sentences) that uses ALL THREE \ of these words: - {w1} - {w2} - {w3} Then phrase the paragraph as a question that explores the connections \ between them. The question should be the kind a curious reader would \ genuinely ask about how these three concepts relate. Output ONLY the question paragraph. No prefix, no commentary, no \ quotes around it, no numbering. Just the question.\ """ def generate_question( client, model_id: str, words: list[str], *, temperature: float = 0.8, max_tokens: int = 256, ) -> str: """Hermes at creative temperature weaves the 3 words into a question.""" prompt = GENERATOR_PROMPT.format(w1=words[0], w2=words[1], w3=words[2]) response = client.chat_completion( messages=[{"role": "user", "content": prompt}], model=model_id, temperature=temperature, max_tokens=max_tokens, ) text = response.strip() if isinstance(response, str) else str(response).strip() # Strip wrapping quotes / leading "Question:" labels in case the # model ignored the no-prefix instruction. text = text.removeprefix("Question:").strip() if text.startswith('"') and text.endswith('"'): text = text[1:-1] return text def run_one_cycle( *, words: list[str], client, model_id: str, qa_db: Path, shards_dir: Path, answer_mode: str, top_k: int, ) -> dict: """One emergent cycle: words → question → answer → log entry dict.""" from arborist.qa.query import DEFAULT_QUERY_POLICY, query t_pick = time.time() question = generate_question(client, model_id, words) t_question = time.time() policy = dict(DEFAULT_QUERY_POLICY) policy["answer_mode"] = answer_mode try: result = query( question=question, qa_db=qa_db, chat_client=client, model_id=model_id, shards_dir=shards_dir, top_k=top_k, policy=policy, burn_existing=True, # always fresh — this is an emergent test ) student_error = None except Exception as e: result = {} student_error = f"{type(e).__name__}: {e}" t_answer = time.time() # Sidecar smell signal: metaphorical question framing the answer # ignored. Surfaced 2026-05-02 by the swallowtail/upbraided/rockiest # emergent log entry. Off-the-binary-chain — gives the teacher # reviewer a flag for poetic-question-vs-literal-answer mismatches. from arborist.qa.inspect import diagnose_metaphor_deflection metaphor = diagnose_metaphor_deflection( question or "", result.get("answer_text") or "" ) return { "ts": int(t_pick), "iso_ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t_pick)), "words": words, "question": question, "question_gen_seconds": round(t_question - t_pick, 2), "answer": result.get("answer_text"), "audit_mode": result.get("audit_mode"), "verifier_method": result.get("verifier_method"), "n_quotes": result.get("n_quotes"), "n_verified": result.get("n_verified"), "violation_kinds": sorted( {v.get("kind") for v in (result.get("violations") or []) if v.get("kind")} ), "metaphor_deflection_kind": metaphor["kind"], "metaphor_cue_count": metaphor["cue_count"], "metaphor_overlap_count": metaphor["answer_overlap_count"], "sources": [ {"title": s.get("title"), "uri": s.get("document_uri"), "used": s.get("used")} for s in (result.get("sources") or []) ], "cache_key": result.get("cache_key"), "answer_mode": answer_mode, "answer_seconds": round(t_answer - t_question, 2), "total_seconds": round(t_answer - t_pick, 2), "student_error": student_error, # Teacher review fields — left null on generation, populated # by a separate review pass (fox brings entries to Opus, gets # back an entry to append to the same log). "teacher": None, } def append_log(entry: dict, log_path: Path) -> None: log_path.parent.mkdir(parents=True, exist_ok=True) with log_path.open("a") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") def emit_unfirehose_session(entry: dict, model_id: str) -> None: """Write one unfirehose/1.0 session for this bench cycle. Same auto-discovery path as `arborist.cli._emit_query_journal`: ~/.arborist/unfirehose/{slug}/{session_uuid}.jsonl. Each cycle gets its own session file (one prompt → one answer = one session). Failures must NEVER break the bench loop — wrapped at the call site. """ from arborist.journal import SessionWriter arborist_meta = { "audit_mode": entry.get("audit_mode"), "verifier_method": entry.get("verifier_method"), "n_quotes": entry.get("n_quotes"), "n_verified": entry.get("n_verified"), "cache_key": entry.get("cache_key"), "violation_kinds": entry.get("violation_kinds"), "metaphor_deflection_kind": entry.get("metaphor_deflection_kind"), "answer_mode": entry.get("answer_mode"), "sources": entry.get("sources"), "timings": { "question_gen_seconds": entry.get("question_gen_seconds"), "answer_seconds": entry.get("answer_seconds"), "total_seconds": entry.get("total_seconds"), }, "bench": "emergent", "words": entry.get("words"), "student_error": entry.get("student_error"), } with SessionWriter(first_prompt=entry.get("question") or "") as s: s.system_message( "bench-emergent cycle: 3-word triangulation", subtype="init", arborist_meta={"words": entry.get("words"), "harness_role": "generator"}, ) s.user_message(entry.get("question") or "") s.assistant_message( entry.get("answer") or "", model=model_id, provider="hermes", duration_ms=int((entry.get("answer_seconds") or 0) * 1000) or None, arborist_meta=arborist_meta, ) def print_pending(log_path: Path) -> int: """Print every log entry with `teacher: None` — what fox should bring to a teacher model for review.""" if not log_path.exists(): print(f"no log at {log_path}") return 0 pending = 0 for line in log_path.read_text().splitlines(): if not line.strip(): continue try: entry = json.loads(line) except json.JSONDecodeError: continue if entry.get("teacher") is None: pending += 1 print(json.dumps(entry, indent=2)) print() # blank line between entries print(f"\n>> {pending} entry/entries awaiting teacher review", file=sys.stderr) return pending def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.strip().splitlines()[0]) parser.add_argument("--n", type=int, default=10, help="cycles to run") parser.add_argument("--seed", type=int, default=None, help="RNG seed for reproducibility") parser.add_argument( "--words-path", type=Path, default=DEFAULT_WORDS_PATH, help="path to a unix-style words file", ) parser.add_argument("--log-path", type=Path, default=DEFAULT_LOG_PATH) parser.add_argument( "--shards-dir", type=Path, default=Path.home() / ".arborist" / "shards", ) parser.add_argument( "--qa-db", type=Path, default=Path.home() / ".arborist" / "shards" / "qa.db", ) parser.add_argument("--answer-mode", default="claim_lattice") parser.add_argument("--top-k", type=int, default=8) parser.add_argument( "--endpoint", default=os.environ.get("ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"), ) parser.add_argument( "--model", default=os.environ.get( "ARBORIST_LLM_MODEL", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", ), ) parser.add_argument( "--print-pending", action="store_true", help="print log entries with teacher==null & exit", ) ns = parser.parse_args(argv) if ns.print_pending: return 0 if print_pending(ns.log_path) >= 0 else 1 if not ns.words_path.exists(): parser.error(f"words file not found: {ns.words_path}") if not ns.shards_dir.exists(): parser.error(f"shards dir not found: {ns.shards_dir}") rng = random.Random(ns.seed) from arborist.qa.client import OpenAICompatibleClient api_key = os.environ.get("ARBORIST_LLM_API_KEY") client = OpenAICompatibleClient(base_url=ns.endpoint, api_key=api_key) print(f">> emergent bench: {ns.n} cycles · seed={ns.seed} · log={ns.log_path}", flush=True) for i in range(ns.n): words = pick_words(3, words_path=ns.words_path, rng=rng) print(f"[{i+1}/{ns.n}] words={words}", flush=True) entry = run_one_cycle( words=words, client=client, model_id=ns.model, qa_db=ns.qa_db, shards_dir=ns.shards_dir, answer_mode=ns.answer_mode, top_k=ns.top_k, ) append_log(entry, ns.log_path) # Mirror to unfirehose-compatible journal so the bench cycles # show up in the unfirehose dashboard alongside Claude Code / # Fetch sessions. Best-effort — never break the bench on a # journal write failure. try: emit_unfirehose_session(entry, ns.model) except Exception: # pragma: no cover pass # Compact stdout summary so a long sweep is observable. audit = entry.get("audit_mode") or "?" ratio = ( f"{entry['n_verified']}/{entry['n_quotes']}" if entry.get("n_quotes") is not None else "?/?" ) err = entry.get("student_error") if err: print(f" → ERROR: {err}", flush=True) else: print( f" → {audit} {ratio} {entry['total_seconds']}s " f"q={entry['question'][:60]!r}", flush=True, ) print(f"\n>> done. log: {ns.log_path}") print( f">> review: bring un-reviewed entries to Opus via\n" f" python scripts/bench_emergent.py --print-pending" ) return 0 if __name__ == "__main__": sys.exit(main())