diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index e71ba50..50f983b 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -232,6 +232,90 @@ class ThinkingEngine: return False # Disabled — never idle return datetime.now(UTC) - self._last_input_time > timedelta(minutes=timeout) + def _build_thinking_context(self) -> tuple[str, str, list["Thought"]]: + """Assemble the context needed for a thinking cycle. + + Returns: + (memory_context, system_context, recent_thoughts) + """ + memory_context = self._load_memory_context() + system_context = self._gather_system_snapshot() + recent_thoughts = self.get_recent_thoughts(limit=5) + return memory_context, system_context, recent_thoughts + + async def _generate_novel_thought( + self, + prompt: str | None, + memory_context: str, + system_context: str, + recent_thoughts: list["Thought"], + ) -> tuple[str | None, str]: + """Run the dedup-retry loop to produce a novel thought. + + Returns: + (content, seed_type) — content is None if no novel thought produced. + """ + seed_type: str = "freeform" + + for attempt in range(self._MAX_DEDUP_RETRIES + 1): + if prompt: + seed_type = "prompted" + seed_context = f"Journal prompt: {prompt}" + else: + seed_type, seed_context = self._gather_seed() + + continuity = self._build_continuity_context() + + full_prompt = _THINKING_PROMPT.format( + memory_context=memory_context, + system_context=system_context, + seed_context=seed_context, + continuity_context=continuity, + ) + + try: + raw = await self._call_agent(full_prompt) + except Exception as exc: + logger.warning("Thinking cycle failed (Ollama likely down): %s", exc) + return None, seed_type + + if not raw or not raw.strip(): + logger.debug("Thinking cycle produced empty response, skipping") + return None, seed_type + + content = raw.strip() + + # Dedup: reject thoughts too similar to recent ones + if not self._is_too_similar(content, recent_thoughts): + return content, seed_type # Good — novel thought + + if attempt < self._MAX_DEDUP_RETRIES: + logger.info( + "Thought too similar to recent (attempt %d/%d), retrying with new seed", + attempt + 1, + self._MAX_DEDUP_RETRIES + 1, + ) + else: + logger.warning( + "Thought still repetitive after %d retries, discarding", + self._MAX_DEDUP_RETRIES + 1, + ) + return None, seed_type + + return None, seed_type + + async def _process_thinking_result(self, thought: "Thought") -> None: + """Run all post-hooks after a thought is stored.""" + self._maybe_check_memory() + await self._maybe_distill() + await self._maybe_file_issues() + await self._check_workspace() + self._maybe_check_memory_status() + self._update_memory(thought) + self._log_event(thought) + self._write_journal(thought) + await self._broadcast(thought) + async def think_once(self, prompt: str | None = None) -> Thought | None: """Execute one thinking cycle. @@ -257,91 +341,21 @@ class ThinkingEngine: ) return None - memory_context = self._load_memory_context() - system_context = self._gather_system_snapshot() - recent_thoughts = self.get_recent_thoughts(limit=5) - - content: str | None = None - seed_type: str = "freeform" - - for attempt in range(self._MAX_DEDUP_RETRIES + 1): - if prompt: - seed_type = "prompted" - seed_context = f"Journal prompt: {prompt}" - else: - seed_type, seed_context = self._gather_seed() - - continuity = self._build_continuity_context() - - full_prompt = _THINKING_PROMPT.format( - memory_context=memory_context, - system_context=system_context, - seed_context=seed_context, - continuity_context=continuity, - ) - - try: - raw = await self._call_agent(full_prompt) - except Exception as exc: - logger.warning("Thinking cycle failed (Ollama likely down): %s", exc) - return None - - if not raw or not raw.strip(): - logger.debug("Thinking cycle produced empty response, skipping") - return None - - content = raw.strip() - - # Dedup: reject thoughts too similar to recent ones - if not self._is_too_similar(content, recent_thoughts): - break # Good — novel thought - - if attempt < self._MAX_DEDUP_RETRIES: - logger.info( - "Thought too similar to recent (attempt %d/%d), retrying with new seed", - attempt + 1, - self._MAX_DEDUP_RETRIES + 1, - ) - content = None # Will retry - else: - logger.warning( - "Thought still repetitive after %d retries, discarding", - self._MAX_DEDUP_RETRIES + 1, - ) - return None + memory_context, system_context, recent_thoughts = self._build_thinking_context() + content, seed_type = await self._generate_novel_thought( + prompt, + memory_context, + system_context, + recent_thoughts, + ) if not content: return None thought = self._store_thought(content, seed_type) self._last_thought_id = thought.id - # Post-hook: check memory status periodically - self._maybe_check_memory() - - # Post-hook: distill facts from recent thoughts periodically - await self._maybe_distill() - - # Post-hook: file Gitea issues for actionable observations - await self._maybe_file_issues() - - # Post-hook: check workspace for new messages from Hermes - await self._check_workspace() - - # Post-hook: proactive memory status audit - self._maybe_check_memory_status() - - # 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) + await self._process_thinking_result(thought) logger.info( "Thought [%s] (%s): %s",