This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/timmy/session_logger.py
Alexander Whitestone a975a845c5 feat: Timmy system introspection, delegation, and session logging (#74)
* test: remove hardcoded sleeps, add pytest-timeout

- Replace fixed time.sleep() calls with intelligent polling or WebDriverWait
- Add pytest-timeout dependency and --timeout=30 to prevent hangs
- Fixes test flakiness and improves test suite speed

* feat: add Aider AI tool to Forge's toolkit

- Add Aider tool that calls local Ollama (qwen2.5:14b) for AI coding assist
- Register tool in Forge's code toolkit
- Add functional tests for the Aider tool

* config: add opencode.json with local Ollama provider for sovereign AI

* feat: Timmy fixes and improvements

## Bug Fixes
- Fix read_file path resolution: add ~ expansion, proper relative path handling
- Add repo_root to config.py with auto-detection from .git location
- Fix hardcoded llama3.2 - now dynamic from settings.ollama_model

## Timmy's Requests
- Add communication protocol to AGENTS.md (read context first, explain changes)
- Create DECISIONS.md for architectural decision documentation
- Add reasoning guidance to system prompts (step-by-step, state uncertainty)
- Update tests to reflect correct model name (llama3.1:8b-instruct)

## Testing
- All 177 dashboard tests pass
- All 32 prompt/tool tests pass

* feat: Timmy system introspection, delegation, and session logging

## System Introspection (Sovereign Self-Knowledge)
- Add get_system_info() tool - Timmy can now query his runtime environment
- Add check_ollama_health() - verify Ollama status
- Add get_memory_status() - check memory tier status
- True introspection vs hardcoded prompts

## Path Resolution Fix
- Fix all toolkits to use settings.repo_root consistently
- Now uses Path(settings.repo_root) instead of Path.cwd()

## Inter-Agent Delegation
- Add delegate_task() tool - Timmy can dispatch to Seer, Forge, Echo, etc.
- Add list_swarm_agents() - query available agents

## Session Logging
- Add SessionLogger for comprehensive interaction logging
- Records messages, tool calls, errors, decisions
- Writes to /logs/session_{date}.jsonl

## Tests
- Add tests for introspection tools
- Add tests for delegation tools
- Add tests for session logging
- Add tests for path resolution
- All 18 new tests pass
- All 177 dashboard tests pass

---------

Co-authored-by: Alexander Payne <apayne@MM.local>
2026-02-27 00:11:53 -05:00

188 lines
5.2 KiB
Python

"""Session logging for Timmy - captures interactions, errors, and decisions.
Timmy requested: "I'd love to see a detailed log of all my interactions,
including any mistakes or errors that occur during the session."
"""
import json
import logging
from datetime import datetime, date
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
class SessionLogger:
"""Logs Timmy's interactions to a session file."""
def __init__(self, logs_dir: str | Path | None = None):
"""Initialize session logger.
Args:
logs_dir: Directory for log files. Defaults to /logs in repo root.
"""
from config import settings
if logs_dir is None:
self.logs_dir = Path(settings.repo_root) / "logs"
else:
self.logs_dir = Path(logs_dir)
# Create logs directory if it doesn't exist
self.logs_dir.mkdir(parents=True, exist_ok=True)
# Session file path
self.session_file = self.logs_dir / f"session_{date.today().isoformat()}.jsonl"
# In-memory buffer
self._buffer: list[dict] = []
def record_message(self, role: str, content: str) -> None:
"""Record a user message.
Args:
role: "user" or "timmy"
content: The message content
"""
self._buffer.append(
{
"type": "message",
"role": role,
"content": content,
"timestamp": datetime.now().isoformat(),
}
)
def record_tool_call(self, tool_name: str, args: dict, result: str) -> None:
"""Record a tool call.
Args:
tool_name: Name of the tool called
args: Arguments passed to the tool
result: Result from the tool
"""
# Truncate long results
result_preview = result[:500] if isinstance(result, str) else str(result)[:500]
self._buffer.append(
{
"type": "tool_call",
"tool": tool_name,
"args": args,
"result": result_preview,
"timestamp": datetime.now().isoformat(),
}
)
def record_error(self, error: str, context: str | None = None) -> None:
"""Record an error.
Args:
error: Error message
context: Optional context about what was happening
"""
self._buffer.append(
{
"type": "error",
"error": error,
"context": context,
"timestamp": datetime.now().isoformat(),
}
)
def record_decision(self, decision: str, rationale: str | None = None) -> None:
"""Record a decision Timmy made.
Args:
decision: What was decided
rationale: Why that decision was made
"""
self._buffer.append(
{
"type": "decision",
"decision": decision,
"rationale": rationale,
"timestamp": datetime.now().isoformat(),
}
)
def flush(self) -> Path:
"""Flush buffer to disk.
Returns:
Path to the session file
"""
if not self._buffer:
return self.session_file
with open(self.session_file, "a") as f:
for entry in self._buffer:
f.write(json.dumps(entry) + "\n")
logger.info("Flushed %d entries to %s", len(self._buffer), self.session_file)
self._buffer.clear()
return self.session_file
def get_session_summary(self) -> dict[str, Any]:
"""Get a summary of the current session.
Returns:
Dict with session statistics
"""
if not self.session_file.exists():
return {
"exists": False,
"entries": 0,
}
entries = []
with open(self.session_file) as f:
for line in f:
if line.strip():
entries.append(json.loads(line))
return {
"exists": True,
"file": str(self.session_file),
"entries": len(entries),
"messages": sum(1 for e in entries if e.get("type") == "message"),
"tool_calls": sum(1 for e in entries if e.get("type") == "tool_call"),
"errors": sum(1 for e in entries if e.get("type") == "error"),
"decisions": sum(1 for e in entries if e.get("type") == "decision"),
}
# Global session logger instance
_session_logger: SessionLogger | None = None
def get_session_logger() -> SessionLogger:
"""Get or create the global session logger."""
global _session_logger
if _session_logger is None:
_session_logger = SessionLogger()
return _session_logger
def get_session_summary() -> dict[str, Any]:
"""Get summary of current session logs.
Returns:
Dict with session statistics (entries, messages, errors, etc.)
"""
logger = get_session_logger()
return logger.get_session_summary()
def flush_session_logs() -> str:
"""Flush current session logs to disk.
Returns:
Path to the log file
"""
logger = get_session_logger()
path = logger.flush()
return str(path)