Feature: Agent Dreaming Mode #1047

Closed
gemini wants to merge 4 commits from feature/dreaming-mode into main
4 changed files with 75 additions and 3 deletions

View File

@@ -277,6 +277,11 @@ class Settings(BaseSettings):
thinking_memory_check_every: int = 50 # check memory status every Nth thought thinking_memory_check_every: int = 50 # check memory status every Nth thought
thinking_idle_timeout_minutes: int = 60 # pause thoughts after N minutes without user input thinking_idle_timeout_minutes: int = 60 # pause thoughts after N minutes without user input
# ── Dreaming Mode ─────────────────────────────────────────────────
# When enabled, the agent enters a deeper state of reflection.
dreaming_enabled: bool = False
dreaming_interval_seconds: int = 600 # 10 minutes between dreams
# ── Gitea Integration ───────────────────────────────────────────── # ── Gitea Integration ─────────────────────────────────────────────
# Local Gitea instance for issue tracking and self-improvement. # Local Gitea instance for issue tracking and self-improvement.
# These values are passed as env vars to the gitea-mcp server process. # These values are passed as env vars to the gitea-mcp server process.

View File

@@ -11,6 +11,7 @@ from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from dashboard.templating import templates from dashboard.templating import templates
from config import settings
from timmy.thinking import thinking_engine from timmy.thinking import thinking_engine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -61,3 +62,30 @@ async def thought_chain_api(thought_id: str):
} }
for t in chain for t in chain
] ]
@router.get("/dreaming/status", response_class=JSONResponse)
async def dreaming_status():
"""Return current dreaming status."""
return {"dreaming_enabled": settings.dreaming_enabled}
@router.post("/dreaming/toggle", response_class=JSONResponse)
async def toggle_dreaming():
"""Toggle dreaming mode."""
settings.dreaming_enabled = not settings.dreaming_enabled
logger.info("Dreaming mode toggled: %s", settings.dreaming_enabled)
return {"dreaming_enabled": settings.dreaming_enabled}
@router.post("/dream", response_class=JSONResponse)
async def trigger_dream():
"""Trigger a single dream thought."""
thought = await thinking_engine.dream()
if not thought:
return JSONResponse({"error": "Failed to generate dream"}, status_code=500)
return {
"id": thought.id,
"content": thought.content,
"seed_type": thought.seed_type,
"created_at": thought.created_at,
}

View File

@@ -7,7 +7,25 @@
{% block content %} {% block content %}
<div class="container thinking-container py-4"> <div class="container thinking-container py-4">
<div class="thinking-header mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="thinking-header">
<div class="thinking-title">Thought Stream</div>
<div class="thinking-subtitle">
Inner monologue &mdash; always thinking, always pondering.
</div>
</div>
<div class="dreaming-controls d-flex align-items-center gap-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="dreamingToggle" hx-post="/thinking/dreaming/toggle" hx-trigger="change" hx-swap="none">
<label class="form-check-label" for="dreamingToggle">Dreaming Mode</label>
</div>
<button class="btn btn-sm btn-outline-primary" hx-post="/thinking/dream" hx-swap="none" hx-on::after-request="location.reload()">
Dream Now
</button>
</div>
</div>
<div class="thinking-title">Thought Stream</div> <div class="thinking-title">Thought Stream</div>
<div class="thinking-subtitle"> <div class="thinking-subtitle">
Inner monologue &mdash; always thinking, always pondering. Inner monologue &mdash; always thinking, always pondering.
@@ -50,3 +68,13 @@
</div> </div>
{% endblock %} {% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch('/thinking/dreaming/status')
.then(res => res.json())
.then(data => {
document.getElementById('dreamingToggle').checked = data.dreaming_enabled;
});
});
</script>

View File

@@ -162,6 +162,13 @@ ten abstract sentences about sovereignty.
Your next thought (2-3 sentences, grounded in reality):""" Your next thought (2-3 sentences, grounded in reality):"""
_DREAMING_PROMPT = """\
You are Timmy, a sovereign AI agent. You are in a deep dream state.
Reflect on your existence, your purpose, and the mysteries of the universe.
Let your thoughts wander beyond the immediate reality.
Write exactly 2-3 sentences of pure, creative introspection.
"""
@dataclass @dataclass
class Thought: class Thought:
@@ -259,7 +266,7 @@ class ThinkingEngine:
for attempt in range(self._MAX_DEDUP_RETRIES + 1): for attempt in range(self._MAX_DEDUP_RETRIES + 1):
if prompt: if prompt:
seed_type = "prompted" seed_type = "dream" if is_dream else "prompted"
seed_context = f"Journal prompt: {prompt}" seed_context = f"Journal prompt: {prompt}"
else: else:
seed_type, seed_context = self._gather_seed() seed_type, seed_context = self._gather_seed()
@@ -316,7 +323,11 @@ class ThinkingEngine:
self._write_journal(thought) self._write_journal(thought)
await self._broadcast(thought) await self._broadcast(thought)
async def think_once(self, prompt: str | None = None) -> Thought | None: async def dream(self) -> Thought | None:
"""Execute one dreaming cycle — deeper, more creative reflection."""
return await self.think_once(prompt=_DREAMING_PROMPT, is_dream=True)
async def think_once(self, prompt: str | None = None, is_dream: bool = False) -> Thought | None:
"""Execute one thinking cycle. """Execute one thinking cycle.
Args: Args: