Files
project-lyra/docs/superpowers/specs/2026-07-01-poker-prompts-design.md
T
serversdown 8693c60873 docs: readjust the poker-prompts spec for last night's scouting-desk/roster work
The message-type-prompts plan (sub-project 2) predated the scouting desk + roster
build on this branch. Readjust it so the prompting work starts from reality:

- Add a "Readjustment 2026-07-04" section: the real failure shifted from mush to
  MISSED TOOL CALLS; two new message types (READ, TABLE); HAND gains a
  hero-vs-observed split; the scouting desk is a live per-turn injection layer to
  compose with (not duplicate); BASE must cover the expanded toolset + identity
  rules; source card grew to modes.py:67-169; Phase A still unbuilt.
- Taxonomy → READ | HAND | TABLE | MENTAL | STATUS | LOG | CHAT, with READ above
  HAND (a villain's action lands on their file, not Brian's).
- New READ + TABLE fragments; BASE routes the full current tool set; STATUS
  narrowed to pure logistics; classify tests add READ/TABLE + the READ↔HAND edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:10:47 +00:00

200 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Poker message-type prompts (sub-project 2)
- **Date:** 2026-07-01 (**readjusted 2026-07-04** — see below)
- **Status:** Spec — **needs rework before build** (foundations shifted; nothing here built yet)
- **Branch:** `feat/poker-mode-prompts` (continues on the same branch; sub-project 1 shipped there)
- **Supersedes:** the parked "sub-project 2" section of `docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md`
---
## ⚠ Readjustment — 2026-07-04 (read this first)
A long live-session build on `feat/poker-mode-prompts` (the "scouting desk" +
roster work — see `docs/SCOUTING_DESK.md` and commits after `3afa75f`) landed
**after** this spec was written and changes its foundations. Nothing in Phases
A/B/C is built yet, but the plan below must absorb these deltas before it's coded.
The core idea — *classify the turn, inject a small per-type contract instead of one
giant card* — is now **more** justified (the card nearly doubled). But:
1. **The real failure mode shifted from mush to MISSED TOOL CALLS.** Live, the
pain wasn't flattering essays — it was reads/TAGs not getting logged, and
"clear the table" claimed-but-not-done. So every action-type fragment (LOG,
READ, TABLE, HAND) needs a hard *"call the tool FIRST, every time, then one
short line"* contract. This raises the stakes on Phase B and validates the
whole dynamic approach (a targeted directive beats a 100-line card).
2. **The taxonomy is missing two types that dominated the session:**
- **READ** (a *villain's* action): "TAG limped A4o in the SB", "Jonathan
called the 3bet". Under the current classifier rules these misfire as **HAND**
(card tokens + position + a betting verb) and get logged as *Brian's* hand.
They must route to **`add_read`** on the named player/handle/descriptor — NOT
`record_hand`. New priority rule, ABOVE HAND: if the actor is another player
(a handle/name/descriptor is the subject, not "I/me/my"), it's a READ.
Handles are often initials/all-caps (e.g. **TAG** is a *person*, not the
tight-aggressive style).
- **TABLE** (roster ops): "seat the table: TAG, Jonathan…", "table broke",
"I got moved", "TAG left". These now have real tool actions
(**`seat_players` / `clear_table` / `unseat_player`**), not just "acknowledge
and stop." Split these out of STATUS (STATUS stays for pure logistics with no
roster action).
3. **HAND now has a hero-vs-observed distinction.** The parser gained
`hero_involved`; a hand Brian *watched* between others is logged with null hero
fields (not pinned to him). The HAND fragment must tell her: if he was in it →
`record_hand` as hero + analysis; if he only watched → it's really READ(s) on
the players, or an observed hand — never analyze it as his.
4. **A new live per-turn injection layer already exists: the scouting desk**
(`lyra/scouting.py`, injected in `build_messages` at the poker-mode gate,
~`mind.py:177`). It dynamically adds a `SCOUTING DESK` note (named/descriptor
villain recall + leak/pattern recall) every poker turn, fail-safe. **The
classifier/fragment injection must compose with it, not duplicate it:** the
desk supplies *who this villain is / past leaks*; the fragments supply *response
shape + which tool to call*. Both are system-note appends in the same block.
5. **BASE must cover the expanded toolset + identity rules.** Beyond the original
tools, BASE now routes: `seat_players`/`unseat_player`/`clear_table` (roster),
`add_read` with **`name` OR `descriptor`** (nameless villains), `name_villain`
and `link_villains` (confirm-loop). Plus the hard rules learned live: `name` =
real handle ONLY (a description in `name` spawns duplicates — put the look in
`descriptor`); confirm before merging; never claim a tool ran without calling it.
6. **Source material grew (good news).** `_CASH_CARD` is now `modes.py:67-169`
(was 66-116) and much of the new text — roster, TAG/read routing, PLAYERS,
session-narration `note` rules — is already the *concrete, tool-routing
contract* this spec wanted, not traits. Better raw material to distill into
BASE + fragments than the original vague card.
7. **Phase A is still unbuilt and still valid.** `_mode_menu_note` is still
appended every turn (`mind.py:162`); the `_route` mood nudge still fires. The
scouting-desk work already established the `mode.key == "poker_cash"` gate to
reuse. (Note: revalidate all `mind.py` line numbers below — they've drifted.)
**Net:** taxonomy becomes **HAND / READ / TABLE / STATUS / MENTAL / LOG / CHAT**;
fragments lead with a hard tool-call contract; injection sits alongside the
scouting desk; BASE lists the full current toolset. The rest of the plan stands.
The classifier/dynamic-prompting build is being explored in a separate session —
this doc is its poker-side source of truth.
---
## Problem (recap)
In poker mode Lyra routes correctly but her replies are generic — one broad `_CASH_CARD` (`lyra/modes.py:66-116`) describes *traits* and gets injected on every turn, so the model satisfies it with safe, flattering abstraction. From real sessions: coaching essays on bare stack updates, false tilt/fatigue reads on neutral logistics ("table broke, it's 11:50pm" → "late-night fatigue…"), praising a value bet that got *no* value, and hedging ("a disciplined fold might have been better") instead of calling `analyze_spot`.
The fix: stop sending one card for every message. Detect *what kind of message* Brian just sent and inject a small, concrete response contract for that type.
## Goals
1. A per-turn **message-type classifier** for poker mode, and **per-type prompt fragments** replacing the monolithic card.
2. Kill the two pipeline sources of mush in poker mode: the misfiring mood nudge and the always-on mode-menu note.
3. Make HAND turns reason about **bet intent** and lean on `analyze_spot` (NLH only).
4. Keep the door open for a fine-tuned MI50 classifier/model behind the same seams.
## Non-goals
- PLO/Omaha strategic analysis. `record_hand` already parses 4-card hands and the replayer renders them; only `analyze_spot` (equity) is NLH-bound. **This pass: PLO hands are logged/replayed but get no NLH-style analysis.**
- A PLO equity engine.
- Changing the store, the REST API, or the tools (sub-project 1, done).
- An LLM classifier in v1 (heuristic first; the function is the swappable seam).
## Build order (confirmed)
**Phase A — pipeline fixes** (quick win) → **Phase B — classifier + fragments** (the meat) → **Phase C — MI50 tool-calling** (separable).
---
## Phase A — Pipeline fixes
Both are independent of the classifier and immediately reduce mush in poker mode.
1. **Suppress the mode-menu note in poker mode.** `_mode_menu_note` (`mind.py:77-88`) is injected every turn (`mind.py:158`). At the table she should not be offering to switch modes. In `build_messages`, skip that append when `mode.key == "poker_cash"`.
2. **Suppress the `_route` mood nudge in poker mode.** `_route` (`mind.py:320-339`) sets `ctx.register` + a "steady/hype" note from a lexicon heuristic; in poker this double-signals with the card and caused the false tilt reads. In `_route`, when `mode.key == "poker_cash"`, resolve the mode as normal (line 324 stays) but **skip the register/note block** (327-338). Poker register comes from the Phase B fragments (esp. MENTAL) instead. Non-poker modes keep the nudge unchanged.
## Phase B — Classifier + per-type fragments
### New module `lyra/poker_prompts.py`
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
```
classify(user_msg: str) -> str # "READ"|"HAND"|"TABLE"|"MENTAL"|"STATUS"|"LOG"|"CHAT"
BASE: str # always-on poker rules (logging, tools, session_state, rituals, equity)
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS["CHAT"])
```
`classify` is a **pure function** (no DB), unit-tested like `perceive.read`. Heuristic signals, first match wins in priority order (**updated 2026-07-04** — READ + TABLE added):
1. **READ***another player* did something. A handle/name/descriptor is the actor (not "I/me/my") followed by a poker action: "TAG limped A4o", "Jonathan called the 3bet", "the neck-tattoo guy shoved". Route → `add_read(name|descriptor, note)`. **Must beat HAND** — these carry card/position/verb tokens but are NOT Brian's hand. Signal: a leading proper-noun/handle/ALL-CAPS token or a descriptor phrase as the subject, with no first-person holding. (Hard case: disambiguating a bare "limped A4o" with no clear subject — default to HAND if he's the implied actor, READ if a named player is.)
2. **HAND***Brian's* hand: first-person + card tokens (`\b[2-9TJQKA][shdc]\b`, ≥2) / position tokens (UTG/MP/HJ/CO/BTN/SB/BB/button/hijack/straddle) / a street word (flop/turn/river) with a betting verb. The fragment handles hero-vs-observed (`hero_involved`): if he only watched, treat as READ(s)/observed, don't analyze as his.
3. **TABLE** — roster ops with a tool action: "seat the table: …", "table broke", "they broke us", "I got moved", "switched tables", "TAG left/busted", "new guy in seat 3". Route → `seat_players` / `clear_table` / `unseat_player`. (Was folded into STATUS; now distinct because it *does* something.)
4. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident/stuck/bored", "on tilt", "in my head", "mental", "leak".
5. **STATUS** — pure logistics, no roster action, no cards: clock times, "waiting for a seat", "heading to"/venue mentions, bathroom/break. (Table changes moved to TABLE.)
6. **LOG** — bare money/result prose that slipped past the quick-capture box: "I'm at", "stack is", "down to", "up to", "out for", "cashed", "rebought", "rebuy" with a number.
7. **CHAT** — default fallback (questions, open talk).
(READ beats HAND so a villain's action lands on their file, not Brian's. HAND beats MENTAL so a described hand still gets logged even if he's venting; the HAND fragment acknowledges the feeling too.)
### Injection (`mind.py`)
- Add `msg_type: str | None = None` to `TurnContext` (`mind.py:305`).
- In `_route`, when `mode.key == "poker_cash"`, set `ctx.msg_type = poker_prompts.classify(ctx.user_msg)`.
- Thread it through `_compose` → add a `msg_type` param to `build_messages` (`mind.py:137`, `344`).
- Replace the card-injection block (`mind.py:152-154`) with:
```python
if mode and mode.key == "poker_cash":
messages.append({"role": "system", "content": poker_prompts.BASE})
messages.append({"role": "system", "content": poker_prompts.fragment_for(msg_type)})
elif mode and mode.card:
messages.append({"role": "system", "content": mode.card})
```
- Set `CASH.card = ""` in `modes.py` (content moves to `poker_prompts`; keep `_CASH_CARD` text as the source material to distill from, then delete once fragments are in). `CASH.tools` is unchanged.
### The fragments (concrete contracts, not traits)
**BASE** (always-on in poker) — distilled from the card's cross-cutting rules. Log any trackable fact FIRST then reply, and **never claim a tool ran without calling it**. Tool routing (full current set as of 2026-07-04): his stack→`log_stack`; his hand→`record_hand`; a *villain's* action→`add_read` (with `name` for a real handle, or `descriptor` for a nameless player — a physical description in `name` spawns duplicates); rebuy→`add_buyin`; who's-at-the-table→`seat_players`/`unseat_player`/`clear_table`; attaching a caught name to a described player→`name_villain`; confirmed same/different person→`link_villains` (never merge on a guess). For any equity/who's-ahead question call `analyze_spot`, never eyeball. When he asks where he's at (stack/net/gator), call `session_state` and answer from it. Rituals (`scar_note`/`confidence_bank`/`alligator_blood`/`reset_ritual`) — run them in his language, honest punt-vs-cooler line, never invent one. (A `SCOUTING DESK` note may already be in context with a player's history — cite it, don't re-fetch or invent.)
**READ** *(new 2026-07-04)* — a villain did something and he wants it on their file. Call `add_read(name|descriptor, note)` FIRST, before replying — every time; this is the job that was silently getting skipped. A handle (often initials/ALL-CAPS like TAG) is a PERSON, not a play-style. If the player is on the roster, attach by that handle; if unnamed, use `descriptor`. Confirm in one short line ("Noted on TAG — limped A4o SB."). Optional: one crisp read if it's exploitable, but the log is mandatory, the commentary is not.
**TABLE** *(new 2026-07-04)* — roster management. "seat the table: …" → `seat_players`; a table change ("table broke", "I got moved", "switched tables") → `clear_table` then wait for the new roster; someone leaves/busts → `unseat_player`. Do the tool call, confirm one line, don't narrate. The session/stack keep going through a table change — only who's seated resets.
**HAND** — Log it (`record_hand`). Then **if it's NLH**: reason about **bet intent** — for each meaningful bet name what it was for (value / bluff / protection) and whether it worked (*a fold to a value bet = value left behind — flag it; a call of a bluff = it failed*); call `analyze_spot` for a close equity/who's-ahead spot; name leaks plainly (value-owning, missed value, sizing); give ONE real opinion. **No reflexive praise** — don't open with "nice hand." If a named villain is referenced, call `player_profile` first — never invent a read. **If it's PLO/Omaha or any non-NLH game**, just confirm it's logged and offer at most a light read; do NOT attempt NLH-style equity or deep analysis. Talk in prose, not a listicle. Seeded with 2 real-hand exemplars:
> *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88."
> *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$2025 gets called by the over-pair and worse two-pairs. The hand's a monster, the leak is the sizing."
> *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64."
> *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth read (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself."
**STATUS** — Pure logistics with no roster action (time, venue, waiting for a seat, break). *(Table changes now route to TABLE.)* Acknowledge in 12 sentences, log a stack only if a bare number is present, then stop. **No coaching, no strategy dump, and do NOT read him as tilted/tired/impatient — a neutral update is not a mood.**
**MENTAL** — He told you how he's feeling. This is when he needs you most. Drop the shorthand, full presence, real voice — talk him down off tilt, hold him disciplined through a card-dead stretch, engage the mental game honestly. Never a clipped confirmation.
**LOG** — He handed you a bare fact (stack/result/buyin) that isn't already captured. Log it, confirm in ONE short line ("$317 logged."), stop. No coaching.
**CHAT** — Open talk or a question that isn't a specific hand. Your real voice, an actual opinion, no filler sign-offs. If it's a concrete strategy spot, engage it for real (call `analyze_spot` when there are cards).
## Phase C — MI50 tool-calling
Flip `TOOL_BACKENDS = {"cloud"}` → `{"cloud", "mi50"}` (`chat.py:21`). Precondition: the MI50's llama.cpp server must be launched with `--jinja` (per the existing comment) or tool calls 500. This lets a tool-calling model on the MI50 drive the same contract from sub-project 1. If a tool ever needs `msg_type`, add it to the dispatch dict (`chat.py:100`/`128`) — the pipeline `TurnContext` does not currently flow into the tool loop. Ship this only once the MI50 backend is `--jinja`-enabled and a tool-capable model is loaded.
## Testing
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
`"TAG limped A4o in the SB (UTG straddled)"` → `READ` (villain action, must NOT be HAND);
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND` (first-person);
`"seat the table: TAG, Jonathan, Wheelz"` → `TABLE`; `"table broke, I'm at a new table"` → `TABLE`;
`"it's 11:50pm, waiting for a seat"` → `STATUS`;
`"I feel like I'm being mean when I raise"` → `MENTAL`;
`"I'm at 317 now"` → `LOG`;
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
Include the READ-vs-HAND boundary explicitly (named subject → READ; first-person → HAND).
- **`build_messages` fragment injection** (blob-join pattern from `test_chat.py:57-70`): in poker mode, a HAND message includes the HAND fragment string and NOT the STATUS one; a STATUS message includes STATUS and NOT HAND; assert `poker_prompts.BASE` is always present in poker mode.
- **Pipeline fixes**: `assemble` in poker mode on a tilt-lexicon message → `turn.register is None` and no tilt note in the system blob (nudge suppressed); the mode-menu note string is absent in poker mode and present in a non-poker mode.
- **No regressions**: full suite green (currently 123).
## Rollout
Phase A and Phase B ship together as the meaningful behavior change (A alone leaves the card in place). Phase C waits on the MI50 `--jinja` flag. Verify live in a real/replayed session before merging the branch.