Home / Engine / Agents and Council Architecture
Updated Jun 20, 2026 · Affirmology_HermesLoop_Bug_v1.md
/api/hermes readings)Jeff ran the two-person wealth-codes question live. After a hard refresh, Hermes returned a FULL, rich, mechanism-level reading for both Jeff and Sol: pulled both charts, ran Lakshmi (and others) deep, cited 8th-house leverage, the Gene Keys Pearls (45.5 Synergy / 49.5 Revolution), the channels, Black Moon Lilith / Chiron blocks, and current transits (Pluto square MC, Uranus trine Sun, Saturn conj ASC). Not a stub. The agentic loop fix is confirmed working on the deployed cloud.
hermesSend in web/app.js no longer flashes the error on a slow/throwing POST. It now polls GET /api/hermes/history?as_person=<id> every 3s for up to 3 min (hermesPollForReply), matches our user turn and renders the assistant turn that follows it, shows a "still composing" note meanwhile, and only errors after the full window. Cache-bust bumped app.js v18 -> v19 so returning browsers fetch it.client.messages.create calls in api/main.py now pass tools=hermes_tools.HERMES_TOOLS (kept, not stripped, the simpler option). The synth prompt still tells the model not to call tools, so it writes the reading as text while the request stays valid even though messages still hold tool_use/tool_result blocks. Reading quality and the oracle model were not touched.
Verified live: origin health 200, app.js carries the polling code, index serves v=19, studio.affirmology.ai healthy behind Access./api/hermes gives up before the slow agentic read finishes, so hermesSend's catch shows "I could not reach my model just now." But the server keeps working and PERSISTS the reply - a refresh surfaces it. FIX (per CLAUDE.md's own guidance): when the POST is slow or errors, POLL GET /api/hermes/history?as_person=<id> for the new turn instead of showing the error. UX-only, no engine change.tools=HERMES_TOOLS OR strip tool_use/tool_result blocks before the tool-free call.Applied all of fixes 1, 2, 3 in affirmology-studio/api/main.py (the hermes() loop) + a defensive corpus-path fix in hermes_tools.py:
- PROMPT (fix 1): _hermes_system now forbids narrating intent ("NEVER ANNOUNCE WHAT YOU ARE ABOUT TO DO... for a reading you MUST actually call consult_oracle before you answer").
- LOOP NUDGE (fix 2): a turn that ends with a narration-only preamble (detected by _is_narration) no longer counts as the answer; the loop appends "act, do not announce" and continues. Round budget raised 6 -> 8, max_tokens 1600 -> 2000.
- FORCED FINAL SYNTHESIS (fix 3): after the loop, a tool-free call always composes the complete reading (handles both the cap-hit and the stub cases, with correct message-role alternation). No more stubs.
- CORPUS (secondary): search_corpus now defaults to the engine's known corpus path if the env is unset, so it queries the same DB the health endpoint reports.
TO ACTIVATE: restart start_public.sh for the local/funnel test, and commit+push api/main.py + api/hermes_tools.py for the cloud. Then re-run the single-person wealth prompt to confirm the oracles fire, then the two-person comparison.
Single-oracle requests now WORK (Sol + Lakshmi returned a real, craft-quality reading using the new points incl. Black Moon Lilith and the Progressed Moon). But MULTI-ORACLE requests (e.g., Jeff asking for 6 oracles) return ok:False "Hermes hit a snag reaching its model (BadRequestError)". Cause: the heavy request exhausts the round budget still in tool_use, so the FORCED FINAL SYNTHESIS call runs, and it omits tools= while the accumulated messages still contain tool_use/tool_result blocks. Anthropic 400s when messages contain tool_use blocks but the request defines no tools. FIX: in the forced-synthesis call, EITHER keep tools=HERMES_TOOLS in the request, OR strip all tool_use/tool_result blocks (and their partner assistant turns) from messages before the tool-free call. Workaround until fixed: keep each /api/hermes reading to 1-2 oracles so the loop finishes naturally.
POST /api/hermes returns a STUB instead of a reading. Three live tests against the running server (funnel up, corpus present, Anthropic key set):
1. Two-person deep wealth request -> reply: "Corpus is quiet today. Moving straight to the oracles with full chart data." (tools_used: none persisted)
2. Jeff-only wealth request -> reply: "Good. Now running Lakshmi and Agastya simultaneously with the full chart data." (tools_used: ['get_person_chart'])
3. Jeff-only with an explicit "do not narrate, actually call consult_oracle for lakshmi/agastya/athena/prometheus/pythagoras and write the full reading" -> reply: "I did not catch that, can you say it another way?" (tools_used: ['get_person_chart'])
In every case Hermes pulls the chart once, then NEVER calls consult_oracle. No oracle ever runs. The user gets a one-line non-answer.
affirmology-studio/api/main.py, the hermes() agentic loop)The loop breaks the moment a model turn comes back with stop_reason != "tool_use":
for _round in range(6):
resp = client.messages.create(... tools=HERMES_TOOLS, messages=messages)
if resp.stop_reason != "tool_use":
break # <-- exits here on a narration-only turn
... run tools, append results ...
parts = [b.text for b in resp.content if b.type == "text"]
reply = "\n".join(parts).strip() or "I did not catch that, can you say it another way?"
Hermes is ending a turn with NARRATION TEXT and stop_reason = "end_turn" (it says "now I'll run the oracles" instead of emitting tool_use blocks). The loop treats that as "done" and returns the narration as the final answer. When the narration turn has no text blocks at all, it returns the "did not catch that" fallback. So the orchestration never reaches the consult-the-oracles-then-synthesize stage.
_hermes_system, forbid narrating intent. Require that each turn either (a) emits tool calls, or (b) writes the final answer. Explicit line: "Never say what you are about to do. Either call the tools now, or give the finished reading. Do not announce steps."end_turn as the final answer when no real reading has been produced. If stop_reason == "end_turn" but no oracle was consulted yet and the text looks like a preamble, append a user nudge ("Now actually call the oracles and write the full reading") and continue the loop instead of breaking.messages.create with tools omitted (or tool_choice disabled) and a system nudge "write the complete reading now from everything gathered." This guarantees a composed answer instead of a stub.tool_choice={"type":"any"} on the early rounds to force tool use until enough oracles have run, then a final free-form synthesis round.Run 1's reply implies the corpus search tool came back empty, yet /api/corpus-stats shows 25,552 records at /Users/jeffreyparker/CLAUDE/AFFIRMOLOGY/corpus/corpus.db across all six traditions. The search_corpus tool is likely not matching (query/embedding or SQL/FTS mismatch, or a path/handle opened before the env was set). Verify the tool actually queries the same DB the health endpoint reports, and that it returns hits for a known term (e.g., "gate 45" or "8th house").
The upgrade added the right ARCHITECTURE (12 oracles incl. Lakshmi for wealth and Agastya for Vedic, corpus grounding, the craft standard in the prompts). But end to end it does not yet produce a reading, because of the loop bug above. Until that is fixed, the agents cannot be compared on quality, because they return stubs. Priority: fix the loop (1+3), re-test with the single-person wealth prompt, then re-run the two-person comparison.
Every long reading (the wealth syntheses, the numerology teaching, the Concordia joint) ends MID-SENTENCE around ~8000 chars. Cause: max_tokens is 2000 in the hermes loop / forced-synthesis call. Deep readings need more. FIX: raise max_tokens for the final synthesis call to ~4096-8192 (or stream and stop on natural end). Jeff noticed the v2 joint section and the Pinnacle teaching both cut off. This is the most visible quality bug right now.