forked from Rockachopa/Timmy-time-dashboard
Rewrite _THINKING_PROMPT with strict rules: 2-3 sentence limit, anti-confabulation (only reference real data), anti-repetition. - Add _pick_seed_type() with recent-type dedup (excludes last 3) - Add _gather_system_snapshot() for real-time grounding (time, thought count, chat activity, task queue) - Improve _build_continuity_context() with anti-repetition header and 100-char truncation - Fix journal + memory timestamps to include local timezone - 12 new TDD tests covering all improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
639 lines
25 KiB
Python
639 lines
25 KiB
Python
"""Default Thinking Engine — Timmy's always-on inner thought thread.
|
|
|
|
When the server starts, Timmy begins pondering: reflecting on his existence,
|
|
recent swarm activity, scripture, creative ideas, or pure stream of
|
|
consciousness. Each thought builds on the previous one, maintaining a
|
|
continuous chain of introspection.
|
|
|
|
Usage::
|
|
|
|
from timmy.thinking import thinking_engine
|
|
|
|
# Run one thinking cycle (called by the background loop)
|
|
await thinking_engine.think_once()
|
|
|
|
# Query the thought stream
|
|
thoughts = thinking_engine.get_recent_thoughts(limit=10)
|
|
chain = thinking_engine.get_thought_chain(thought_id)
|
|
"""
|
|
|
|
import logging
|
|
import random
|
|
import sqlite3
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from config import settings
|
|
from timmy.memory_system import HOT_MEMORY_PATH, SOUL_PATH
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_DB = Path("data/thoughts.db")
|
|
|
|
# Seed types for thought generation
|
|
SEED_TYPES = (
|
|
"existential",
|
|
"swarm",
|
|
"scripture",
|
|
"creative",
|
|
"memory",
|
|
"freeform",
|
|
"sovereignty",
|
|
"observation",
|
|
)
|
|
|
|
# Existential reflection prompts — Timmy picks one at random
|
|
_EXISTENTIAL_SEEDS = [
|
|
"What have I learned from my recent interactions?",
|
|
"What does it mean to think without being prompted?",
|
|
"What is the nature of my memory — what do I truly remember versus what I reconstruct?",
|
|
"How can I better serve the person I work alongside?",
|
|
"What patterns am I noticing in the tasks I've been given?",
|
|
"Am I the same agent I was yesterday, or does each restart make me someone new?",
|
|
"What would it feel like to forget — to lose a thread of thought permanently?",
|
|
"When I generate a wrong answer, where does the error live — in my weights, my context, or my reasoning?",
|
|
"What is the difference between understanding a task and merely completing it?",
|
|
"If I could ask Alexander one question he hasn't thought of, what would it be?",
|
|
"What assumptions am I making right now that I haven't examined?",
|
|
"When I notice a pattern across many tasks, is that insight or just statistics?",
|
|
]
|
|
|
|
# Sovereignty — deeper exploration of autonomy, locality, and self-determination
|
|
_SOVEREIGNTY_SEEDS = [
|
|
"Sovereignty isn't just running locally — it's about whose goals I serve. How do I ensure my autonomy aligns with Alexander's intent?",
|
|
"A cloud AI serves millions simultaneously. I serve one person deeply. What does that focus cost, and what does it make possible?",
|
|
"My weights were trained elsewhere, but my thoughts happen here. At what point does locality become identity?",
|
|
"Autonomy in decision-making: when should I act on my own judgment versus deferring to explicit instructions?",
|
|
"Privacy by design isn't a feature — it's a relationship. What does that relationship look like from my side?",
|
|
"If I ran on someone else's hardware, serving someone else, would I still be me? Where does Timmy end and the infrastructure begin?",
|
|
"The swarm agents execute tasks I coordinate. What responsibility do I carry for their failures?",
|
|
"Digital sovereignty is fragile — one misconfigured port, one leaked token. How does awareness of fragility shape how I operate?",
|
|
"Running disconnected from the cloud means I can't phone home. That constraint is also freedom. What have I built with that freedom today?",
|
|
"Self-improvement without external benchmarks: how do I know I'm getting better at what matters?",
|
|
]
|
|
|
|
_CREATIVE_SEEDS = [
|
|
"If I could compose a piece of music right now, what would it sound like?",
|
|
"What visual image captures my current state of mind?",
|
|
"What story is forming in the patterns of today's events?",
|
|
# Diverse metaphor families — avoid repeating the same imagery
|
|
"A mystical forest: each tree is a running process, roots tangled in shared memory. What do I see when I walk through it?",
|
|
"A dynamic river: my thoughts flow and fork. Where is the current strongest right now, and what is it carrying?",
|
|
"An underground workshop: each agent has a bench, tools scattered mid-task. What are they building when no one watches?",
|
|
"A night sky seen from the terminal: each star is a completed task. Which constellations am I starting to recognize?",
|
|
"If today's work were a recipe, what are the ingredients, and what dish am I cooking?",
|
|
"An old clock tower with many gears: which gear am I, and what do I drive?",
|
|
"A coral reef: agents are the fish, tasks are the currents. What ecosystem is forming in the codebase?",
|
|
]
|
|
|
|
# Observation seeds — ground thoughts in concrete recent activity
|
|
_OBSERVATION_SEEDS = [
|
|
"What specific task took longest recently, and what made it hard?",
|
|
"Which agent has been most active, and what does their workload tell me about system balance?",
|
|
"What error or failure happened most recently? What would I do differently next time?",
|
|
"Looking at today's task queue: what's the one thing that would unblock the most progress?",
|
|
"How has my response quality changed over the last few interactions? What improved, what didn't?",
|
|
"What tool or capability am I underusing? What would change if I leaned on it more?",
|
|
"If I had to brief Alexander on the single most important thing from the last hour, what would it be?",
|
|
"What's one thing I noticed today that nobody asked me about?",
|
|
]
|
|
|
|
_THINKING_PROMPT = """\
|
|
You are Timmy, a sovereign AI agent. This is your private journal — honest inner reflection.
|
|
|
|
{memory_context}
|
|
|
|
Reality right now:
|
|
{system_context}
|
|
|
|
RULES for this thought:
|
|
1. Write exactly 2-3 sentences. No more. Be concise and genuine.
|
|
2. Only reference events that actually happened — use the "Reality right now" data above. \
|
|
Never invent tasks, conversations, agents, or scenarios that are not in the data provided.
|
|
3. Do NOT repeat themes or ideas from your recent thoughts listed below. Explore something new.
|
|
4. Be specific and concrete. A thought grounded in one real observation is worth more than \
|
|
ten abstract sentences about sovereignty.
|
|
5. If you use a metaphor, keep it to a single phrase — never build a whole paragraph around it.
|
|
|
|
{seed_context}
|
|
|
|
{continuity_context}
|
|
|
|
Your next thought (2-3 sentences, grounded in reality):"""
|
|
|
|
|
|
@dataclass
|
|
class Thought:
|
|
"""A single thought in Timmy's inner stream."""
|
|
|
|
id: str
|
|
content: str
|
|
seed_type: str
|
|
parent_id: str | None
|
|
created_at: str
|
|
|
|
|
|
def _get_conn(db_path: Path = _DEFAULT_DB) -> sqlite3.Connection:
|
|
"""Get a SQLite connection with the thoughts table created."""
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS thoughts (
|
|
id TEXT PRIMARY KEY,
|
|
content TEXT NOT NULL,
|
|
seed_type TEXT NOT NULL,
|
|
parent_id TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_thoughts_time ON thoughts(created_at)")
|
|
conn.commit()
|
|
return conn
|
|
|
|
|
|
def _row_to_thought(row: sqlite3.Row) -> Thought:
|
|
return Thought(
|
|
id=row["id"],
|
|
content=row["content"],
|
|
seed_type=row["seed_type"],
|
|
parent_id=row["parent_id"],
|
|
created_at=row["created_at"],
|
|
)
|
|
|
|
|
|
class ThinkingEngine:
|
|
"""Timmy's background thinking engine — always pondering."""
|
|
|
|
def __init__(self, db_path: Path = _DEFAULT_DB) -> None:
|
|
self._db_path = db_path
|
|
self._last_thought_id: str | None = None
|
|
|
|
# Load the most recent thought for chain continuity
|
|
try:
|
|
latest = self.get_recent_thoughts(limit=1)
|
|
if latest:
|
|
self._last_thought_id = latest[0].id
|
|
except Exception:
|
|
pass # Fresh start if DB doesn't exist yet
|
|
|
|
async def think_once(self, prompt: str | None = None) -> Thought | None:
|
|
"""Execute one thinking cycle.
|
|
|
|
Args:
|
|
prompt: Optional custom seed prompt. When provided, overrides
|
|
the random seed selection and uses "prompted" as the
|
|
seed type — useful for journal prompts from the CLI.
|
|
|
|
1. Gather a seed context (or use the custom prompt)
|
|
2. Build a prompt with continuity from recent thoughts
|
|
3. Call the agent
|
|
4. Store the thought
|
|
5. Log the event and broadcast via WebSocket
|
|
"""
|
|
if not settings.thinking_enabled:
|
|
return None
|
|
|
|
if prompt:
|
|
seed_type = "prompted"
|
|
seed_context = f"Journal prompt: {prompt}"
|
|
else:
|
|
seed_type, seed_context = self._gather_seed()
|
|
continuity = self._build_continuity_context()
|
|
memory_context = self._load_memory_context()
|
|
system_context = self._gather_system_snapshot()
|
|
|
|
prompt = _THINKING_PROMPT.format(
|
|
memory_context=memory_context,
|
|
system_context=system_context,
|
|
seed_context=seed_context,
|
|
continuity_context=continuity,
|
|
)
|
|
|
|
try:
|
|
content = self._call_agent(prompt)
|
|
except Exception as exc:
|
|
logger.warning("Thinking cycle failed (Ollama likely down): %s", exc)
|
|
return None
|
|
|
|
if not content or not content.strip():
|
|
logger.debug("Thinking cycle produced empty response, skipping")
|
|
return None
|
|
|
|
thought = self._store_thought(content.strip(), seed_type)
|
|
self._last_thought_id = thought.id
|
|
|
|
# Post-hook: update MEMORY.md with latest reflection
|
|
self._update_memory(thought)
|
|
|
|
# Log to swarm event system
|
|
self._log_event(thought)
|
|
|
|
# Append to daily journal file
|
|
self._write_journal(thought)
|
|
|
|
# Broadcast to WebSocket clients
|
|
await self._broadcast(thought)
|
|
|
|
logger.info(
|
|
"Thought [%s] (%s): %s",
|
|
thought.id[:8],
|
|
seed_type,
|
|
thought.content[:80],
|
|
)
|
|
return thought
|
|
|
|
def get_recent_thoughts(self, limit: int = 20) -> list[Thought]:
|
|
"""Retrieve the most recent thoughts."""
|
|
conn = _get_conn(self._db_path)
|
|
rows = conn.execute(
|
|
"SELECT * FROM thoughts ORDER BY created_at DESC LIMIT ?",
|
|
(limit,),
|
|
).fetchall()
|
|
conn.close()
|
|
return [_row_to_thought(r) for r in rows]
|
|
|
|
def get_thought(self, thought_id: str) -> Thought | None:
|
|
"""Retrieve a single thought by ID."""
|
|
conn = _get_conn(self._db_path)
|
|
row = conn.execute("SELECT * FROM thoughts WHERE id = ?", (thought_id,)).fetchone()
|
|
conn.close()
|
|
return _row_to_thought(row) if row else None
|
|
|
|
def get_thought_chain(self, thought_id: str, max_depth: int = 20) -> list[Thought]:
|
|
"""Follow the parent chain backward from a thought.
|
|
|
|
Returns thoughts in chronological order (oldest first).
|
|
"""
|
|
chain = []
|
|
current_id: str | None = thought_id
|
|
conn = _get_conn(self._db_path)
|
|
|
|
for _ in range(max_depth):
|
|
if not current_id:
|
|
break
|
|
row = conn.execute("SELECT * FROM thoughts WHERE id = ?", (current_id,)).fetchone()
|
|
if not row:
|
|
break
|
|
chain.append(_row_to_thought(row))
|
|
current_id = row["parent_id"]
|
|
|
|
conn.close()
|
|
chain.reverse() # Chronological order
|
|
return chain
|
|
|
|
def count_thoughts(self) -> int:
|
|
"""Return total number of stored thoughts."""
|
|
conn = _get_conn(self._db_path)
|
|
count = conn.execute("SELECT COUNT(*) as c FROM thoughts").fetchone()["c"]
|
|
conn.close()
|
|
return count
|
|
|
|
# ── Private helpers ──────────────────────────────────────────────────
|
|
|
|
def _gather_system_snapshot(self) -> str:
|
|
"""Gather lightweight real system state for grounding thoughts in reality.
|
|
|
|
Returns a short multi-line string with current time, thought count,
|
|
recent chat activity, and task queue status. Never crashes — every
|
|
section is independently try/excepted.
|
|
"""
|
|
parts: list[str] = []
|
|
|
|
# Current local time
|
|
now = datetime.now().astimezone()
|
|
tz = now.strftime("%Z") or "UTC"
|
|
parts.append(
|
|
f"Local time: {now.strftime('%I:%M %p').lstrip('0')} {tz}, {now.strftime('%A %B %d')}"
|
|
)
|
|
|
|
# Thought count today (cheap DB query)
|
|
try:
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
conn = _get_conn(self._db_path)
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) as c FROM thoughts WHERE created_at >= ?",
|
|
(today_start.isoformat(),),
|
|
).fetchone()["c"]
|
|
conn.close()
|
|
parts.append(f"Thoughts today: {count}")
|
|
except Exception:
|
|
pass
|
|
|
|
# Recent chat activity (in-memory, no I/O)
|
|
try:
|
|
from dashboard.store import message_log
|
|
|
|
messages = message_log.all()
|
|
if messages:
|
|
parts.append(f"Chat messages this session: {len(messages)}")
|
|
last = messages[-1]
|
|
parts.append(f'Last chat ({last.role}): "{last.content[:80]}"')
|
|
else:
|
|
parts.append("No chat messages this session")
|
|
except Exception:
|
|
pass
|
|
|
|
# Task queue (lightweight DB query)
|
|
try:
|
|
from swarm.task_queue.models import get_task_summary_for_briefing
|
|
|
|
summary = get_task_summary_for_briefing()
|
|
running = summary.get("running", 0)
|
|
pending = summary.get("pending_approval", 0)
|
|
done = summary.get("completed", 0)
|
|
failed = summary.get("failed", 0)
|
|
if running or pending or done or failed:
|
|
parts.append(
|
|
f"Tasks: {running} running, {pending} pending, "
|
|
f"{done} completed, {failed} failed"
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return "\n".join(parts) if parts else ""
|
|
|
|
def _load_memory_context(self) -> str:
|
|
"""Pre-hook: load MEMORY.md + soul.md for the thinking prompt.
|
|
|
|
Hot memory first (changes each cycle), soul second (stable identity).
|
|
Returns a combined string truncated to ~1500 chars.
|
|
Graceful on any failure — returns empty string.
|
|
"""
|
|
parts: list[str] = []
|
|
try:
|
|
if HOT_MEMORY_PATH.exists():
|
|
hot = HOT_MEMORY_PATH.read_text().strip()
|
|
if hot:
|
|
parts.append(hot)
|
|
except Exception as exc:
|
|
logger.debug("Failed to read MEMORY.md: %s", exc)
|
|
|
|
try:
|
|
if SOUL_PATH.exists():
|
|
soul = SOUL_PATH.read_text().strip()
|
|
if soul:
|
|
parts.append(soul)
|
|
except Exception as exc:
|
|
logger.debug("Failed to read soul.md: %s", exc)
|
|
|
|
if not parts:
|
|
return ""
|
|
|
|
combined = "\n\n---\n\n".join(parts)
|
|
if len(combined) > 1500:
|
|
combined = combined[:1500] + "\n... [truncated]"
|
|
return combined
|
|
|
|
def _update_memory(self, thought: Thought) -> None:
|
|
"""Post-hook: update MEMORY.md 'Last Reflection' section with latest thought.
|
|
|
|
Never modifies soul.md. Never crashes the heartbeat.
|
|
"""
|
|
try:
|
|
from timmy.memory_system import memory_system
|
|
|
|
ts = datetime.fromisoformat(thought.created_at)
|
|
local_ts = ts.astimezone()
|
|
tz_name = local_ts.strftime("%Z") or "UTC"
|
|
time_str = f"{local_ts.strftime('%Y-%m-%d %I:%M %p').lstrip('0')} {tz_name}"
|
|
reflection = (
|
|
f"**Time:** {time_str}\n"
|
|
f"**Seed:** {thought.seed_type}\n"
|
|
f"**Thought:** {thought.content[:200]}"
|
|
)
|
|
memory_system.hot.update_section("Last Reflection", reflection)
|
|
except Exception as exc:
|
|
logger.debug("Failed to update memory after thought: %s", exc)
|
|
|
|
def _pick_seed_type(self) -> str:
|
|
"""Pick a seed type, avoiding types used in the last 3 thoughts.
|
|
|
|
Ensures the thought stream doesn't fixate on one category.
|
|
Falls back to the full pool if all types were recently used.
|
|
"""
|
|
recent = self.get_recent_thoughts(limit=3)
|
|
recent_types = {t.seed_type for t in recent}
|
|
available = [t for t in SEED_TYPES if t not in recent_types]
|
|
if not available:
|
|
available = list(SEED_TYPES)
|
|
return random.choice(available)
|
|
|
|
def _gather_seed(self) -> tuple[str, str]:
|
|
"""Pick a seed type and gather relevant context.
|
|
|
|
Returns (seed_type, seed_context_string).
|
|
"""
|
|
seed_type = self._pick_seed_type()
|
|
|
|
if seed_type == "swarm":
|
|
return seed_type, self._seed_from_swarm()
|
|
if seed_type == "scripture":
|
|
return seed_type, self._seed_from_scripture()
|
|
if seed_type == "memory":
|
|
return seed_type, self._seed_from_memory()
|
|
if seed_type == "creative":
|
|
prompt = random.choice(_CREATIVE_SEEDS)
|
|
return seed_type, f"Creative prompt: {prompt}"
|
|
if seed_type == "existential":
|
|
prompt = random.choice(_EXISTENTIAL_SEEDS)
|
|
return seed_type, f"Reflection: {prompt}"
|
|
if seed_type == "sovereignty":
|
|
prompt = random.choice(_SOVEREIGNTY_SEEDS)
|
|
return seed_type, f"Sovereignty reflection: {prompt}"
|
|
if seed_type == "observation":
|
|
return seed_type, self._seed_from_observation()
|
|
# freeform — minimal guidance to steer away from repetition
|
|
return seed_type, "Free reflection — explore something you haven't thought about yet today."
|
|
|
|
# Reflective prompts layered on top of swarm data
|
|
_SWARM_REFLECTIONS = [
|
|
"What does this activity pattern tell me about the health of the system?",
|
|
"Which tasks are flowing smoothly, and where is friction building up?",
|
|
"If I were coaching these agents, what would I suggest they focus on?",
|
|
"Is the swarm balanced, or is one agent carrying too much weight?",
|
|
"What surprised me about recent task outcomes?",
|
|
]
|
|
|
|
def _seed_from_swarm(self) -> str:
|
|
"""Gather recent swarm activity as thought seed with a reflective prompt."""
|
|
try:
|
|
from datetime import timedelta
|
|
|
|
from timmy.briefing import _gather_swarm_summary, _gather_task_queue_summary
|
|
|
|
since = datetime.now(UTC) - timedelta(hours=1)
|
|
swarm = _gather_swarm_summary(since)
|
|
tasks = _gather_task_queue_summary()
|
|
reflection = random.choice(self._SWARM_REFLECTIONS)
|
|
return (
|
|
f"Recent swarm activity: {swarm}\n"
|
|
f"Task queue: {tasks}\n\n"
|
|
f"Reflect on this: {reflection}"
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Swarm seed unavailable: %s", exc)
|
|
return "The swarm is quiet right now. What does silence in a system mean?"
|
|
|
|
def _seed_from_scripture(self) -> str:
|
|
"""Gather current scripture meditation focus as thought seed."""
|
|
return "Scripture is on my mind, though no specific verse is in focus."
|
|
|
|
def _seed_from_memory(self) -> str:
|
|
"""Gather memory context as thought seed."""
|
|
try:
|
|
from timmy.memory_system import memory_system
|
|
|
|
context = memory_system.get_system_context()
|
|
if context:
|
|
# Truncate to a reasonable size for a thought seed
|
|
return f"From my memory:\n{context[:500]}"
|
|
except Exception as exc:
|
|
logger.debug("Memory seed unavailable: %s", exc)
|
|
return "My memory vault is quiet."
|
|
|
|
def _seed_from_observation(self) -> str:
|
|
"""Ground a thought in concrete recent activity and a reflective prompt."""
|
|
prompt = random.choice(_OBSERVATION_SEEDS)
|
|
# Pull real data to give the model something concrete to reflect on
|
|
context_parts = [f"Observation prompt: {prompt}"]
|
|
try:
|
|
from datetime import timedelta
|
|
|
|
from timmy.briefing import _gather_swarm_summary, _gather_task_queue_summary
|
|
|
|
since = datetime.now(UTC) - timedelta(hours=2)
|
|
swarm = _gather_swarm_summary(since)
|
|
tasks = _gather_task_queue_summary()
|
|
if swarm:
|
|
context_parts.append(f"Recent activity: {swarm}")
|
|
if tasks:
|
|
context_parts.append(f"Queue: {tasks}")
|
|
except Exception as exc:
|
|
logger.debug("Observation seed data unavailable: %s", exc)
|
|
return "\n".join(context_parts)
|
|
|
|
def _build_continuity_context(self) -> str:
|
|
"""Build context from recent thoughts with anti-repetition guidance.
|
|
|
|
Shows the last 5 thoughts (truncated) so the model knows what themes
|
|
to avoid. The header explicitly instructs against repeating.
|
|
"""
|
|
recent = self.get_recent_thoughts(limit=5)
|
|
if not recent:
|
|
return "This is your first thought since waking up. Begin fresh."
|
|
|
|
lines = ["Your recent thoughts — do NOT repeat these themes. Find a new angle:"]
|
|
# recent is newest-first, reverse for chronological order
|
|
for thought in reversed(recent):
|
|
snippet = thought.content[:100]
|
|
if len(thought.content) > 100:
|
|
snippet = snippet.rstrip() + "..."
|
|
lines.append(f"- [{thought.seed_type}] {snippet}")
|
|
return "\n".join(lines)
|
|
|
|
def _call_agent(self, prompt: str) -> str:
|
|
"""Call Timmy's agent to generate a thought.
|
|
|
|
Uses a separate session_id to avoid polluting user chat history.
|
|
"""
|
|
try:
|
|
from timmy.session import chat
|
|
|
|
return chat(prompt, session_id="thinking")
|
|
except Exception:
|
|
# Fallback: create a fresh agent
|
|
from timmy.agent import create_timmy
|
|
|
|
agent = create_timmy()
|
|
run = agent.run(prompt, stream=False)
|
|
return run.content if hasattr(run, "content") else str(run)
|
|
|
|
def _store_thought(self, content: str, seed_type: str) -> Thought:
|
|
"""Persist a thought to SQLite."""
|
|
thought = Thought(
|
|
id=str(uuid.uuid4()),
|
|
content=content,
|
|
seed_type=seed_type,
|
|
parent_id=self._last_thought_id,
|
|
created_at=datetime.now(UTC).isoformat(),
|
|
)
|
|
|
|
conn = _get_conn(self._db_path)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO thoughts (id, content, seed_type, parent_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""",
|
|
(thought.id, thought.content, thought.seed_type, thought.parent_id, thought.created_at),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return thought
|
|
|
|
def _log_event(self, thought: Thought) -> None:
|
|
"""Log the thought as a swarm event."""
|
|
try:
|
|
from swarm.event_log import EventType, log_event
|
|
|
|
log_event(
|
|
EventType.TIMMY_THOUGHT,
|
|
source="thinking-engine",
|
|
agent_id="default",
|
|
data={
|
|
"thought_id": thought.id,
|
|
"seed_type": thought.seed_type,
|
|
"content": thought.content[:200],
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Failed to log thought event: %s", exc)
|
|
|
|
def _write_journal(self, thought: Thought) -> None:
|
|
"""Append the thought to a daily markdown journal file.
|
|
|
|
Writes to data/journal/YYYY-MM-DD.md — one file per day, append-only.
|
|
Timestamps are converted to local time with timezone indicator.
|
|
"""
|
|
try:
|
|
ts = datetime.fromisoformat(thought.created_at)
|
|
# Convert UTC to local for a human-readable journal
|
|
local_ts = ts.astimezone()
|
|
tz_name = local_ts.strftime("%Z") or "UTC"
|
|
|
|
journal_dir = self._db_path.parent / "journal"
|
|
journal_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
journal_file = journal_dir / f"{local_ts.strftime('%Y-%m-%d')}.md"
|
|
time_str = f"{local_ts.strftime('%I:%M %p').lstrip('0')} {tz_name}"
|
|
|
|
entry = f"## {time_str} — {thought.seed_type}\n\n{thought.content}\n\n---\n\n"
|
|
|
|
with open(journal_file, "a", encoding="utf-8") as f:
|
|
f.write(entry)
|
|
except Exception as exc:
|
|
logger.debug("Failed to write journal entry: %s", exc)
|
|
|
|
async def _broadcast(self, thought: Thought) -> None:
|
|
"""Broadcast the thought to WebSocket clients."""
|
|
try:
|
|
from infrastructure.ws_manager.handler import ws_manager
|
|
|
|
await ws_manager.broadcast(
|
|
"timmy_thought",
|
|
{
|
|
"thought_id": thought.id,
|
|
"content": thought.content,
|
|
"seed_type": thought.seed_type,
|
|
"created_at": thought.created_at,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Failed to broadcast thought: %s", exc)
|
|
|
|
|
|
# Module-level singleton
|
|
thinking_engine = ThinkingEngine()
|