Remove debug pane

This commit is contained in:
Dejvino 2026-07-03 21:26:24 +02:00
parent 52deb1db6a
commit 0458811e02
8 changed files with 4 additions and 246 deletions

View File

@ -28,7 +28,6 @@ class GameEngine:
on_thought: callable = None,
on_action: callable = None,
on_player_roll: callable = None,
on_debug: callable = None,
) -> TurnResult:
now = datetime.now()
state.append_llm_log(f"\n{'='*60}")
@ -48,8 +47,6 @@ class GameEngine:
if on_action:
on_action("DM is preparing a response")
if on_debug:
on_debug("config", {"model": model, "temperature": lm.get("temperature"), "max_tokens": lm.get("max_tokens"), "strategy": "tools"})
system = build_system_prompt(recent_narrative=recent_narrative, recent_log=session_log)
base_parts = []
@ -89,7 +86,6 @@ class GameEngine:
text = call_llm(
[{"role": "system", "content": system}, {"role": "user", "content": user}],
label="Turn generation",
on_debug=on_debug,
)
if not text or not text.strip():
@ -102,7 +98,7 @@ class GameEngine:
raw = text.strip()
state.append_llm_log(f"\n[TOOL] got {len(raw)} chars in {(datetime.now() - start_time).total_seconds() * 1000:.1f}ms")
tool_calls = extract_tool_calls(raw, on_debug=on_debug)
tool_calls = extract_tool_calls(raw)
if not tool_calls:
state.append_llm_log("\n[TOOL] no tool blocks found")
@ -141,12 +137,8 @@ class GameEngine:
changes=state_changes,
story=recent_narrative,
log=session_log,
on_debug=on_debug,
)
if on_debug:
on_debug("turn_validation", {"valid": valid, "reason": reason, "action": action, "attempt": total_attempts})
if valid:
state.append_llm_log(f"\n[TURN VALID] {reason}")
elif reason == "Unrecognized":
@ -219,19 +211,6 @@ class GameEngine:
if on_action:
on_action("Turn complete")
if on_debug:
applied = len([tc for tc in tool_calls if tc.get("tool") not in ("finalize_turn", "narrative")])
on_debug("phase_done", {
"book_log_chars": len(book_log),
"log_entry": log_entry,
"ambience": ambience,
"extract_errors": errors or None,
"total_elapsed_ms": total_elapsed,
"tool_calls_count": len(tool_calls),
"applied_changes_count": applied,
"tool_call_results": tool_calls,
"total_attempts": total_attempts,
})
log_turn_details(
player_action=player_action or "",
@ -245,7 +224,6 @@ class GameEngine:
log_entry=log_entry or "",
ambience=ambience,
tool_calls=tool_calls,
on_debug=on_debug,
)
return TurnResult(

View File

@ -30,7 +30,6 @@ def call_llm(
timeout: int | None = None,
max_tokens: int | None = None,
label: str = "",
on_debug: callable = None,
) -> str | None:
"""Make a single LLM call. Loads config automatically. Returns content text or None on error."""
from .config import load_config
@ -45,8 +44,6 @@ def call_llm(
try:
import litellm
except ImportError:
if on_debug:
on_debug("llm_error", {"label": label, "error": "litellm not installed"})
return None
try:
response = litellm.completion(
@ -67,6 +64,4 @@ def call_llm(
except Exception as e:
err_msg = f"{type(e).__name__}: {e}"
append_llm_log(f"\n--- LLM ERROR ({label}) ---\n{err_msg}")
if on_debug:
on_debug("llm_error", {"label": label, "error": err_msg})
return None

View File

@ -18,13 +18,11 @@ def log_turn_details(
log_entry: str,
ambience: Optional[str],
tool_calls: list,
on_debug=None,
) -> None:
"""Write structured turn summary to llm.log and fire TUI debug event."""
ts = datetime.now().isoformat()
output_chars = len(book_log)
output_words = len(book_log.split()) if book_log else 0
applied = len([tc for tc in tool_calls if tc.get("tool") not in ("finalize_turn", "narrative")])
state.append_llm_log("")
state.append_llm_log(f"┌─ Turn Details — {ts}")
@ -41,20 +39,3 @@ def log_turn_details(
state.append_llm_log(
"└─────────────────────────────────────────────────────────────────────────────────────────┘"
)
if on_debug:
on_debug("turn_details", {
"timestamp": ts,
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
"strategy_name": strategy_name,
"die_roll": die_roll,
"player_action": player_action,
"book_log_chars": output_chars,
"book_log_words": output_words,
"ambience": ambience,
"tool_calls_count": len(tool_calls),
"applied_changes_count": applied,
"tool_call_results": tool_calls,
})

View File

@ -230,7 +230,7 @@ def describe_change(tool_name: str, args: dict) -> str:
return ""
def extract_tool_calls(text: str, on_debug: callable = None) -> list[dict]:
def extract_tool_calls(text: str) -> list[dict]:
"""Extract tool calls from ```tool blocks in LLM response."""
calls = []
seen = set()

View File

@ -51,7 +51,6 @@ def validate_action(
*,
story: str = "",
log: str = "",
on_debug: callable = None,
) -> tuple[bool, str]:
"""Ask the LLM whether a player action is valid given the game state. Returns (valid, reason)."""
if not player_action:
@ -72,7 +71,6 @@ def validate_action(
max_tokens=1024,
temperature=0.2,
label="Action validation",
on_debug=on_debug,
)
if not text:
@ -86,12 +84,8 @@ def validate_action(
data = json.loads(cleaned)
valid = data.get("valid", True)
reason = data.get("reason", "")
if on_debug:
on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action})
return valid, reason
except (json.JSONDecodeError, ValueError):
if on_debug:
on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
if attempt == 0:
messages.append({
"role": "system",
@ -198,7 +192,6 @@ def validate_turn(
changes: list[dict] | None = None,
story: str = "",
log: str = "",
on_debug: callable = None,
) -> tuple[bool, str, str]:
"""Validate a complete generated turn.
@ -229,7 +222,6 @@ def validate_turn(
max_tokens=1024,
temperature=0.2,
label="Turn validation",
on_debug=on_debug,
)
if not text:
@ -250,12 +242,8 @@ def validate_turn(
action = args.get("action", "ok")
if action not in ("ok", "reject", "regenerate"):
action = "ok" if valid else "reject"
if on_debug:
on_debug("turn_validation", {"valid": valid, "reason": reason, "action": action})
return valid, reason, action
except (json.JSONDecodeError, ValueError):
if on_debug:
on_debug("turn_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
if attempt == 0:
messages.append({
"role": "system",

View File

@ -28,7 +28,7 @@ from run_utils import (
from run_ambience import AmbiencePlayer
from run_widgets import (
app_ambience_player as _widget_player_ref,
RollModal, DebugPane, CharPane, StatusBar, TodoPane, TranscriptPane,
RollModal, CharPane, StatusBar, TodoPane, TranscriptPane,
)
@ -66,10 +66,6 @@ class ChaosTUI(App):
VerticalScroll { overflow-y: auto; scrollbar-size-vertical:2; scrollbar-color:#555; scrollbar-color-hover:#777; scrollbar-color-active:#999; }
#char-content { background: #1e1e2a; color: #c0c0c0; padding: 0 1; }
#transcript { background: #1a2a1a; color: #c8c8c8; padding: 0 1; }
#debug-content { background: #1a1a1a; color: #88b0a0; padding: 0 1; }
#debug-content .dm-thought { color: #c0a060; }
#debug-content .dm-tool { color: #60a0c0; }
#debug-content .dm-result { color: #80a080; }
#play-narrative { background: #161616; color: #d8d8d8; padding: 1 2; height: auto; }
#play-status { background: #1a2a1a; color: #e0b060; padding: 0 2; height: 1; text-style: bold italic; text-align: center; }
#play-status.processing { background: #2a1a0a; color: #ffd93d; }
@ -171,9 +167,6 @@ class ChaosTUI(App):
yield Button("Next >>", id="book-next")
with VerticalScroll(id="book-scroll"):
yield Static("", id="book-content")
with TabPane("DEBUG", id="debug-tab"):
with VerticalScroll():
yield DebugPane("", id="debug-content")
yield StatusBar(id="status-bar")
yield Button("", id="mute-btn", classes="mute-button")
@ -247,8 +240,6 @@ class ChaosTUI(App):
input_widget.disabled = True
input_widget.placeholder = "DM is at work..."
self._show_thinking()
self.query_one("#debug-content", DebugPane).clear()
self._append_debug(f"{'player action: ' + player_action if player_action else 'starting new turn'}")
t = threading.Thread(target=self._run_generation, args=(player_action,), daemon=True)
t.start()
@ -260,27 +251,18 @@ class ChaosTUI(App):
def on_action(action: str) -> None:
self.call_from_thread(self._on_action, action)
def on_debug(event_type: str, data: dict) -> None:
self.call_from_thread(self._on_debug, event_type, data)
try:
result = self.engine.generate_turn(
player_action=player_action,
on_thought=on_thought,
on_action=on_action,
on_player_roll=self._on_player_roll,
on_debug=on_debug,
)
except Exception as e:
self.call_from_thread(self._on_generation_error, e, traceback.format_exc())
return
self.call_from_thread(self._on_generation_done, result, player_action)
def _append_debug(self, text: str) -> None:
from datetime import datetime
ts = datetime.now().strftime("%H:%M:%S")
self.query_one("#debug-content", DebugPane).append(f"[{ts}] {text}")
def _show_thinking(self) -> None:
self._dm_action = "DM is preparing a response"
self._thinking_frame = 0
@ -304,7 +286,6 @@ class ChaosTUI(App):
status = self.query_one("#play-status", Static)
status.add_class("processing")
status.update(f"{self._spinner_frames[0]} {display}")
self._append_debug(f"{display}")
def _on_action(self, action: str) -> None:
self._dm_action = action
@ -312,91 +293,8 @@ class ChaosTUI(App):
status = self.query_one("#play-status", Static)
status.add_class("processing")
status.update(f"{self._spinner_frames[0]} {action}")
self._append_debug(action)
def _on_debug(self, event_type: str, data: dict) -> None:
if event_type == "phase":
p = data.get("phase", 0)
status = data.get("status", "")
if status == "start":
n = data.get("name", "")
d = f" dice={data['dice']}" if data.get("dice") else ""
o = f" [attempt {data['outer_attempt']}/3]" if data.get("outer_attempt") else ""
self._append_debug(f"▸ Phase {p}: {n}{o}{d}")
elif status == "done":
if p == 1:
self._append_debug(f" ✔ prose: {data.get('chars', 0)} chars")
elif p == 2:
self._append_debug(f" ✔ summary: {data.get('summary', '')}")
elif p == 3:
self._append_debug(f" ✔ extract: {data.get('applied', 0)} state changes applied")
elif status == "empty":
self._append_debug(f" ⚠ phase {p} attempt {data.get('attempt', '?')} empty — retry")
elif status == "fallback":
self._append_debug(f" ⚠ phase {p} used fallback: {data.get('summary', '')}")
elif status == "tools_found":
tools = data.get("tools", [])
t = ", ".join(tools) if tools else "none"
self._append_debug(f" 🔧 tools: {t}" + (" + finalize" if data.get("has_finalize") else ""))
elif status == "errors":
for e in data.get("errors", []):
self._append_debug(f"{e}")
self._append_debug(f" ⟳ retry ({data.get('attempt', '?')})")
elif status == "exhausted":
self._append_debug(" ✖ Phase 3 exhausted retries — state changes may be missing!")
for e in data.get("errors", []):
self._append_debug(f" {e}")
elif status == "retry_after_phase3_failure":
self._append_debug(f" ⟳ Phase 3 failed — retry from Phase 1 (attempt {data.get('outer_attempt', '?')}/3)")
elif status == "validation_failed":
self._append_debug(f" ✖ narrative rejected: {data.get('reason', '?')} (attempt {data.get('outer_attempt', '?')}/3)")
elif event_type == "phase_done":
self._append_debug(f" ✔ turn — book_log: {data.get('book_log_chars', 0)} chars")
if data.get("log_entry"):
self._append_debug(f" log: {data['log_entry']}")
if data.get("ambience"):
self._append_debug(f" ambience: {data['ambience']}")
if data.get("extract_errors"):
self._append_debug(f" extract errors: {data['extract_errors']}")
elif event_type == "config":
m = data.get("model", "?")
t = data.get("temperature", "?")
tk = data.get("max_tokens", "?")
s = data.get("strategy", "?")
self._append_debug(f"▸ LLM: {m} | temp={t} | tokens={tk} | strategy={s}")
elif event_type == "tool_call":
self._append_debug(f" 🔧 {data['tool']}({json.dumps(data.get('args', {}))})")
elif event_type == "tool_result":
r = data.get("result", "")
self._append_debug(f"{r[:80].replace(chr(10),' ').strip()}{'' if len(r)>80 else ''}")
elif event_type == "parse_error":
self._append_debug(f" ⚠ bad tool block: {data.get('content', '')}")
elif event_type == "llm_error":
self._append_debug(f" ✖ LLM [{data.get('label','')}]: {data.get('error','')}")
elif event_type == "turn_details":
ts = data.get("timestamp", "")
m = data.get("model", "?")
t = data.get("temperature", "?")
tk = data.get("max_tokens", "?")
s = data.get("strategy_name", "?")
d = data.get("die_roll", "?")
ic = len(data.get("player_action", ""))
oc = data.get("book_log_chars", 0)
w = data.get("book_log_words", 0)
a = data.get("ambience", "None")
tc = data.get("tool_calls_count", 0)
ap = data.get("applied_changes_count", 0)
tms = data.get("total_elapsed_ms", 0)
ams = data.get("apply_elapsed_ms", 0)
self._append_debug(f" ━━━ Turn ━━━ {ts}")
self._append_debug(f" LLM: {m} | temp={t} | tokens={tk} | strategy={s}")
self._append_debug(f" Dice: {d} | In: {ic}c | Out: {oc}c ({w}w)")
self._append_debug(f" Amb: {a} | Tools: {tc} (applied: {ap}) | Time: {tms:.0f}ms ({ams:.0f}ms apply)")
for tc2 in data.get("tool_call_results", []):
self._append_debug(f" 🔧 {tc2['tool']}: {json.dumps(tc2.get('args',{}))[:120]}")
def _on_player_roll(self, dice: str, reason: str) -> str:
self.call_from_thread(self._append_debug, f"🎲 roll {dice} ({reason})")
self.call_from_thread(self._show_roll_modal, dice, reason)
self._roll_event.wait()
self._roll_event.clear()
@ -426,7 +324,6 @@ class ChaosTUI(App):
self._hide_thinking()
if result.error:
self._show_error(result.error, result.debug_info)
self._append_debug(f"✖ error: {result.error}")
return
if result.book_log:
turn_num = state.archive_turn(result.book_log)
@ -435,6 +332,7 @@ class ChaosTUI(App):
else:
summary = result.book_log.strip().split(chr(10))[0][:80]
state.append_log(f"- **Turn {turn_num}** — {summary}")
result.book_log = load_book_pages()[-1]
elif result.log_entry:
state.append_log(f"- {result.log_entry}")
state.apply_state(result)
@ -448,15 +346,11 @@ class ChaosTUI(App):
self._display_scene(result)
if result.book_log:
self._last_result = result
self._append_debug("✔ turn complete")
def _on_generation_error(self, error: Exception, traceback_str: str) -> None:
self._is_processing = False
self._hide_thinking()
err_msg = f"{type(error).__name__}: {error}"
self._append_debug(f"✖ UNHANDLED: {err_msg}")
for line in traceback_str.rstrip().split("\n")[-10:]:
self._append_debug(f" {line}")
self._show_error(err_msg, traceback_str)
@staticmethod

View File

@ -112,29 +112,6 @@ class TranscriptPane(AutoStatic):
self.parent.scroll_end(animate=False)
class DebugPane(Static):
MAX_LINES = 200
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._lines: list[str] = []
def append(self, text: str) -> None:
self._lines.append(text)
if len(self._lines) > self.MAX_LINES:
self._lines.pop(0)
self.update("\n".join(self._lines[-100:]))
self.call_after_refresh(self._scroll_bottom)
def _scroll_bottom(self):
if self.parent and hasattr(self.parent, 'scroll_end'):
self.parent.scroll_end(animate=False)
def clear(self) -> None:
self._lines.clear()
self.update("")
class CharPane(AutoStatic):
def load(self):
if not CHAR_PATH.exists():

View File

@ -104,29 +104,6 @@ def test_missing_character_sheet(mock_truncate_world, mock_read_file):
print("✓ handles missing character sheet gracefully")
@patch("engine_lib.validation.state.read_file")
@patch("engine_lib.validation.state.truncate_world")
@patch("engine_lib.validation.call_llm")
def test_on_debug_called(mock_call_llm, mock_truncate_world, mock_read_file):
from engine_lib.validation import validate_action
mock_read_file.side_effect = lambda p: "HP: 10" if "character" in str(p).lower() else "## Location\nTavern"
mock_truncate_world.return_value = "## Location\nTavern"
mock_call_llm.return_value = json.dumps({"valid": True, "reason": "ok"})
events = []
def debug_cb(key, data):
events.append((key, data))
valid, reason = validate_action("I open the door", story="In a hallway", log="- Heard noises", on_debug=debug_cb)
assert valid is True
assert len(events) == 1
assert events[0][0] == "action_validation"
assert events[0][1]["valid"] is True
print("✓ on_debug callback receives action_validation event")
# ── validate_turn tests ────────────────────────────────────
@ -273,36 +250,6 @@ def test_turn_bad_json(mock_call_llm, mock_truncate_world, mock_read_file):
print("✓ turn validation bad JSON gives (False, 'Unrecognized', 'reject')")
@patch("engine_lib.validation.state.read_file")
@patch("engine_lib.validation.state.truncate_world")
@patch("engine_lib.validation.call_llm")
def test_turn_on_debug(mock_call_llm, mock_truncate_world, mock_read_file):
from engine_lib.validation import validate_turn
mock_read_file.side_effect = _mock_read_basic
mock_truncate_world.return_value = "## Location\nTavern"
mock_call_llm.return_value = _tool_response(True, "ok", "ok")
events = []
def debug_cb(key, data):
events.append((key, data))
valid, reason, action = validate_turn(
"I open the door",
narrative="Dillion opens the door.",
log_entry="Dillion opened the door and entered the hall.",
story="In a hallway",
log="- Heard noises",
on_debug=debug_cb,
)
assert valid is True
assert len(events) == 1
assert events[0][0] == "turn_validation"
assert events[0][1]["valid"] is True
print("✓ on_debug callback receives turn_validation event")
if __name__ == "__main__":
test_empty_action()
test_valid_action()
@ -310,11 +257,9 @@ if __name__ == "__main__":
test_llm_returns_none()
test_llm_returns_bad_json()
test_missing_character_sheet()
test_on_debug_called()
test_turn_empty_inputs()
test_turn_valid()
test_turn_reject()
test_turn_regenerate()
test_turn_bad_json()
test_turn_on_debug()
print("\n✓ All validation tests passed")