| """The Warden's voice: live LLM dialogue with scripted fallback. |
| |
| The contract with the UI: react() yields a sanitized line; if the backend |
| is slow, dead, or rude, it yields nothing and the caller plays a scripted |
| line instead. The game never stalls and never shows raw output. |
| |
| Lines are buffered and sanitized BEFORE any text is released — partial |
| streaming could leak the prefix of a blocked line. The UI's typewriter |
| provides the gradual reveal instead, so nothing is lost dramatically. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import random |
| from typing import AsyncIterator |
|
|
| from scrypt.inference.backend import Backend |
|
|
| from .context import build_messages |
| from .guardrails import clean_dialogue |
| from .memory import ShardStore |
|
|
| TOTAL_TIMEOUT = 25.0 |
| |
| |
| REVEAL_ODDS = 1 / 3 |
| THINKING_MAX_TOKENS = 192 |
|
|
|
|
| class WardenVoice: |
| def __init__( |
| self, |
| backend: Backend, |
| store: ShardStore | None = None, |
| rng: random.Random | None = None, |
| ): |
| self.backend = backend |
| self.store = store or ShardStore() |
| self.rng = rng or random.Random() |
|
|
| async def react( |
| self, |
| moment: str, |
| *, |
| digest: str = "", |
| tags: set[str] | None = None, |
| taboo: str = "", |
| ) -> AsyncIterator[str]: |
| """Stream a one-liner reaction to a game moment. taboo: raw player |
| text the reply must never parrot (echo-exfiltration guard).""" |
| messages = build_messages( |
| f"MOMENT\n{moment}\n\nReact in one short line of at most 18 words, in voice.", |
| digest=digest, |
| shards=self.store.render(tags or set()), |
| ) |
| thinking = self.rng.random() < REVEAL_ODDS |
| buffer = "" |
| try: |
| async with asyncio.timeout(TOTAL_TIMEOUT): |
| async for chunk in self.backend.stream( |
| messages, |
| max_tokens=THINKING_MAX_TOKENS if thinking else 48, |
| temperature=0.6, |
| thinking=thinking, |
| ): |
| buffer += chunk |
| except Exception: |
| pass |
| cleaned = clean_dialogue(buffer, taboo=taboo) |
| if cleaned: |
| yield cleaned |
|
|