Phase 1: Permission modal dialog, session resume modal, HistoryInput with up/down arrow cycling, remove "You:" echo from chat log, LLM client cleanup on unmount. Phase 2: Smart shell auto-approve using allowed/denied command lists from ToolsConfig, animated thinking spinner with live token count in status bar. Phase 3: /models slash command (list + switch), CLI directory positional argument, JSONL debug logger with rotation. Phase 4: Skills system with SkillsManager, load_skill LLM tool, /skills listing, skill invocation via slash commands, system prompt integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""Debug logger — writes detailed LLM interaction logs to JSONL files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.models.message import Message
|
|
|
|
|
|
class DebugLogger:
|
|
"""Writes detailed LLM interaction logs to JSONL files for debugging."""
|
|
|
|
def __init__(self, log_dir: Path, max_files: int = 10) -> None:
|
|
self._log_dir = log_dir
|
|
self._log_dir.mkdir(parents=True, exist_ok=True)
|
|
self._max_files = max_files
|
|
self._file = self._log_dir / f"debug_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.jsonl"
|
|
self._rotate()
|
|
|
|
def log_request(self, messages: list[Message], model: str) -> None:
|
|
"""Log outbound LLM request (message roles/lengths, model)."""
|
|
self._write({
|
|
"event": "llm_request",
|
|
"model": model,
|
|
"message_count": len(messages),
|
|
"messages": [
|
|
{"role": m.role, "content_len": len(m.content or "")}
|
|
for m in messages
|
|
],
|
|
})
|
|
|
|
def log_response(
|
|
self,
|
|
message: Message,
|
|
usage: Any | None,
|
|
elapsed_ms: float,
|
|
) -> None:
|
|
"""Log LLM response with timing and token counts."""
|
|
self._write({
|
|
"event": "llm_response",
|
|
"elapsed_ms": round(elapsed_ms, 1),
|
|
"content_len": len(message.content or ""),
|
|
"tool_call_count": len(message.tool_calls or []),
|
|
"tool_calls": [tc.function.name for tc in (message.tool_calls or [])],
|
|
"usage": usage.__dict__ if usage else None,
|
|
})
|
|
|
|
def log_tool_execution(
|
|
self,
|
|
tool_name: str,
|
|
result_status: str,
|
|
elapsed_ms: float,
|
|
) -> None:
|
|
"""Log tool execution with timing."""
|
|
self._write({
|
|
"event": "tool_execution",
|
|
"tool": tool_name,
|
|
"status": result_status,
|
|
"elapsed_ms": round(elapsed_ms, 1),
|
|
})
|
|
|
|
def _write(self, record: dict[str, Any]) -> None:
|
|
record["timestamp"] = datetime.now(UTC).isoformat()
|
|
with open(self._file, "a") as f:
|
|
f.write(json.dumps(record) + "\n")
|
|
|
|
def _rotate(self) -> None:
|
|
"""Remove old debug log files beyond max_files."""
|
|
files = sorted(
|
|
self._log_dir.glob("debug_*.jsonl"),
|
|
key=lambda p: p.stat().st_mtime,
|
|
)
|
|
while len(files) > self._max_files:
|
|
files.pop(0).unlink()
|