Running a chat capability / benchmark test manually
A runbook for driving dream-chat.html through a prompt suite (golden benchmark or freeform capability list) and scoring the results — written down so the next run skips the legwork this one paid for. Every gotcha below was hit live on 2026-07-09 while running HumanEval-chat + a 20-prompt capability suite.
TL;DR — the fast path
- Server: start the
lantern-devpreview config (server-dev.js, port 4178,
CHAT_TOOL_EXEC=1). Confirm curl http://127.0.0.1:4178/api/health is UP and /api/providers/status shows at least one provider hasKey:true.
- Warm the provider once (see gotcha #1) with a throwaway turn before you
start counting — the first turn of a cold session can spuriously error.
- Drive plain turns. Set
#input, dispatch aninputevent, click#send-btn.
Do not pass routeIntent: "coding_change" unless you are specifically testing the coder route — it stalls in the local-first dev config (gotcha #2). The router classifies coding prompts correctly on its own.
- Pace off the disk log, not the DOM (gotcha #3 + #4): after each send, poll
data/conversations/garage-conversations.jsonl for the new role:"lantern" row that follows your role:"operator" prompt. This survives the 30s preview_eval cap and a mid-run server crash.
- Grade:
- Golden coding benchmark → reuse the real sandbox:
from eval_humaneval_ouro import make_candidate, run_test (in scripts/). Wrap the reply as a ```python fence and run the canonical unit test.
- Open-ended prompts → LLM-judged (no ground truth). Only a few are objectively
checkable (fact-check, code-review, SQL, curl→requests) — verify those hard.
For the fully automated coding path, scripts/eval_humaneval_chat.py already drives POST /api/dream/chat/stream over allproblems and writes a data/eval/leaderboard.jsonl row. Use it for the headline number; use this manual recipe when you want the browser path or a non-coding suite.
Gotchas (each cost real time — don't rediscover them)
- Cold-start provider error. The first message of a fresh session (or the
first after a restart) can return "No AI providers are set up" / "AI unavailable" even when /api/providers/status shows all keys present — provider health is still "untested". It self-heals on the next turn. Warm with one throwaway send first. Tracked in #2128.
routeIntent: "coding_change"hangs whenKEYSTONE_SERVE_OURO=1but no
Ouro is actually served: the SSE emits only the route event, then nothing. Plain turns route around it. Tracked in #2321.
preview_evalhas a hard 30s cap. A batch runner or any single slow turn
(long generations run 20–30s) will time out the eval even though the send already fired. Don't batch turns inside one eval — send one at a time, fire-and-forget, and recover the reply from the disk log.
- The server can crash mid-run on a long generation and take all in-page JS
state with it (preview_list empties, port goes dead). The conversation log data/conversations/garage-conversations.jsonl is the source of truth — every turn persists there (capped atchars/entry). Rebuild results from it. Tracked in #2320.
- Rendered code blocks use
<br>for newlines.textContentcollapses them
and destroys Python indentation. When extracting from the DOM, replace <br> → \n on innerHTML, strip tags, then decode entities. (The disk log stores clean text, so pacing off disk sidesteps this entirely.)
- Groundedness bands are noisy on closed-context tasks. A faithful
"summarize this provided text" answer came back red. Don't treat the band as a pass/fail gate when judging. Tracked in #2322.
- Placeholders/attachments. Many capability prompts reference an attachment or
a [placeholder]. Supply a small inline stand-in (dataset, transcript, snippet) so the turn is runnable, and note in the report that a real attachment would change the result.
Tool gating — measure the faculty, not the tool (#2777)
Burnell et al. (arXiv:2605.28405 §4.3, canon [07]): "if the system can simply search the internet … we are no longer measuring the system's memory — only its ability to search." So when an eval targets a specific faculty, disable the tools that would let the model substitute a different capability — otherwise the score is uninterpretable (this is the #2322 closed-context groundedness-noise class).
Faculty under test → tools to gate:
| Eval condition (loop stage) | Gate these tools | Why |
|---|---|---|
| Memory / recall (Remember) | web_search, web_fetch |
else recall is replaced by live search |
| Reasoning, closed-context (Reason) | web_search, web_fetch, and repo retrieval (Read/Grep/workspace_read) |
the answer must come from the model, not lookup |
| Coding, no-lookup (Act) | web_search, web_fetch |
measures generation, not retrieval of a known solution |
How to gate a run. Set CHAT_EVAL_GATED_TOOLS (comma/space-separated tool names) before the run — or pass ctx.gatedTools to runTool:
CHAT_EVAL_GATED_TOOLS="web_search,web_fetch" node scripts/eval_humaneval_chat.py # example
runTool then denies each gated tool before it executes, returning reason_code: "tool_gated", and the denial is written to the tool log — so the run record shows exactly which tools were gated and that no gated call slipped through. Names are case-insensitive. Leave the variable unset (or blank) for a normal, ungated run.
Selectors & storage reference
| Thing | Where |
|---|---|
| Input textarea | #input (dispatch input event after setting .value) |
| Send button | #send-btn |
| Assistant messages | #messages .message.agent → .message-content |
| Groundedness band | .message-content[data-groundedness-band] (green/amber/red) |
| Persisted turns | data/conversations/garage-conversations.jsonl (role: operator=human, lantern=assistant) |
| HumanEval dataset (offline) | HF_HOME=D:\hf-cache, HF_DATASETS_OFFLINE=1 |
| Coding grader | scripts/eval_humaneval_ouro.py → make_candidate, run_test |
| Full coding harness | scripts/eval_humaneval_chat.py (writes data/eval/leaderboard.jsonl) |