19 KiB
DivaCode — AI Being Platform Action Plan
Language: Go
Inference: llama.cpp (HTTP server), OpenAI-compatible API planned
Storage: SQLite (primary), PostgreSQL + pgvector (future)
Inspiration: OpenHer, Agent Diva, nanobot
Current Status (2026-06-10)
Builds: yes
Runs: yes (requires llama.cpp on :8080)
Known issues:
- Prompt template is raw text — model dumps reasoning/self-correction meta-text instead of just the response
- No chat template configurability (ChatML, Llama 3 instruct, etc.)
- TUI is basic (no streaming, no word-wrap polish)
- Memory store uses keyword
LIKEsearch, not real embeddings - OpenAI-compatible adapter not yet built
- Matrix adapter is a stub
Architecture Overview
User
│
├── TUI (Bubble Tea)
└── Matrix (future)
│
┌─────┴──────────────────────────────┐
│ Core Agent Loop │
│ (internal/agent/) │
│ - Message routing │
│ - Tool invocation │
│ - Context management │
└─────┬──────────────────────────────┘
│
┌─────┴──────────────────────────────┐
│ Companion Layer │
│ (internal/companion/) │
│ ├─ Personality (5 bounded traits) │
│ ├─ Relationship (familiarity,trust)│
│ ├─ Reflection (diary,observations)│
│ ├─ Mood (6 states + energy) │
│ └─ Prompt Composer (dynamic ctx) │
└─────┬──────────────────────────────┘
│
┌─────┴──────────────────────────────┐
│ Memory Layer │
│ (internal/memory/) │
│ ├─ Short-term (ephemeral context) │
│ ├─ Long-term (SQLite → pgvector) │
│ ├─ Episodic / Diary (sqlite) │
│ └─ Self-memory (promises,strats) │
└─────┬──────────────────────────────┘
│
┌─────┴──────────────────────────────┐
│ Inference (internal/llama/) │
│ ├─ llama.cpp HTTP client │
│ └─ OpenAI-compatible (planned) │
└────────────────────────────────────┘
Skills / Tools:
├─ web_fetch
├─ web_search (stub)
└─ MCP protocol (future)
├─ Obsidian vault
├─ Gitea
├─ Actual Budget
└─ Custom MCP tools
Phase 0 — Foundation & Scaffolding
Status: COMPLETE
- Go module:
git.db123.ir/db123/divacode - Project structure per Holy Code Bible conventions
internal/config/— std libos.Getenvonlyinternal/storage/—modernc.org/sqlite(pure Go, no CGO), auto-createsdata/dirinternal/llama/client.go— HTTP client forPOST /completion,POST /embedding,GET /health.env.example,Makefile,.gitignore,Dockerfile,docker-compose.ymlAGENTS.mdwith architecture, commands, conventionspkg/types/types.go— Message, Role, Mood, ToolCall typespkg/personality/traits.go— TraitValue, RelationshipMetrics, DiaryEntry, Observation types
Phase 1 — Core Agent Loop
Status: COMPLETE (minor issues remain)
internal/agent/agent.go— message loop: receive → memory search → prompt → llama → save → respondinternal/agent/scheduler.go— background goroutine, configurable tick (default 15m), mood check, diary trigger, autonomous message queuecmd/divad/main.go— wires everything: config, DB, llama health, TUI, scheduler, Matrix, API- Conversation persistence to SQLite (
conversations,messagestables with indexes) - Bubble Tea TUI with viewport + input bar, Ctrl+C/Ctrl+Q quit
- HTTP API (std lib
net/http):GET /health,POST /v1/chat,POST /v1/conversations - Prompt template is raw headers — model outputs self-correction/next-action meta-text instead of just the response
- No token tracking — context window grows unbounded
- TUI doesn't stream tokens (waits for full completion)
Schema (internal/storage/migrations.go):
CREATE TABLE conversations (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL REFERENCES conversations(id),
role TEXT NOT NULL CHECK(role IN ('user','assistant','system','tool')),
content TEXT NOT NULL,
tool_call TEXT,
token_count INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
CREATE TABLE context_state (
conversation_id TEXT PRIMARY KEY REFERENCES conversations(id),
token_count INTEGER NOT NULL DEFAULT 0,
summary TEXT
);
Phase 2 — Prompt Templating & Reasoning-Aware TUI
Goal: Fix model output with proper chat templates. Parse reasoning/thoughts from the response and render them beautifully in the TUI — dimmed, collapsible, like OpenCode.
Chat Template Engine
internal/llama/template.go— chat template formatter:- ChatML:
<|im_start|>system\n...<|im_end|>\n<|im_start|>user\n...<|im_end|>\n<|im_start|>assistant\n - Llama 3 instruct:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n...<|eot_id|> - Configurable via env
CHAT_TEMPLATE=chatml|llama3|none(default: chatml)
- ChatML:
- Refactor
companion/promptbuilder.goto output structuredBuildResult - Let template.go wrap those segments in the chosen format
- Add
stoptokens per template (<|im_end|>,<|eot_id|>)
Reasoning Parser
internal/agent/reasoning.go— parse<think>,<reasoning>,<reason>,<thinking>tags- Split response into
Reasoning(thoughts) andContent(final answer) - Store both separately in the
messagestable - Feed only
Contentback into context window (reasoning is discarded after display)
TUI Enhancement
- Render reasoning in a collapsible section (dimmed/italic,
lipgloss.Color("#6B7280")) - Render final answer in normal assistant style (green/bold)
- Reasoning hidden by default, toggle with
tabkey - Mouse support (
tea.WithMouseCellMotion()+ viewport scroll)
Command Palette & Themes
internal/channel/palette.go— fuzzy-findable command palette overlay (Ctrl+P):- Modal popup with search/filter input
- Renders a filtered list of commands with descriptions
- Enter to execute, Esc to dismiss
/help— show available commands with descriptions/new— start a new conversation (alias:/clear)/exit— quit the TUI (alias:/quit,/q)/thinking— toggle visibility of reasoning/thinking blocks in the conversation/themes— list and switch between color themes/sessions— list and switch between conversations (alias:/resume)internal/channel/theme.go— theme engine:- JSON-based theme config with
defs+themecolor map - Built-in themes:
default,tokyonight,catppuccin,gruvbox,nord,matrix - Uses
THEMEenv var or/themescommand to switch - Hues:
primary,secondary,accent,text,textMuted,background,border,success,error
- JSON-based theme config with
internal/channel/sessions.go— session management:- List existing conversations from SQLite
- Switch agent's active conversation
- Show per-session message count and last activity
Phase 3 — Long-Term Memory (RAG)
Goal: Embedding-based retrieval so the agent recalls facts across sessions.
internal/memory/embed.go— call llama.cppPOST /embeddingto generate vectors- SQLite vector storage: store embeddings as BLOB in
memoriestable internal/memory/store.go— upgradeSearch()from keywordLIKEto cosine similarity scan- Temporal decay — older memories scored lower (weight = importance * recency_factor)
- Importance scoring — user flags ("remember this"), emotional content, repeat mentions boost score
CREATE TABLE memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
embedding BLOB, -- float32 vector
source TEXT, -- 'conversation', 'reflection', 'user_input'
importance REAL DEFAULT 0.5, -- 0.0–1.0
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_accessed TEXT
);
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
- Future: Migrate to PostgreSQL + pgvector for production-scale vector search
- Test: Store info, restart agent, ask about stored info → verify recall
Phase 4 — OpenAI-Compatible Adapter
Goal: Swap inference to any OpenAI-compatible API (OpenAI, vLLM, Ollama, Groq, etc.).
internal/llama/provider.go— interface:
type Provider interface {
Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error)
Embed(ctx context.Context, content string) ([]float64, error)
Health() error
}
- Rename
internal/llama/→internal/inference/ llama.go— implements Provider (existing llama.cpp client wrapped)openai.go— implements Provider via OpenAI-compatible REST API (/v1/chat/completions,/v1/embeddings)- Config:
INFERENCE_PROVIDER=llama|openai,OPENAI_API_KEY,OPENAI_BASE_URL,OPENAI_MODEL - Both providers produce same interface, switchable via env var
- Test: Same conversation, swap provider, verify behavior is consistent
Phase 5 — Episodic Diary & Reflection
Goal: Daily diary entries + periodic self-reflection for narrative continuity. The diary is the emotional continuity layer.
Status: CODE DONE — needs LLM integration for real summaries.
internal/companion/reflection.go—GenerateDiary,RecentObservations,AddObservationdiary_entriestable:
CREATE TABLE diary_entries (
id TEXT PRIMARY KEY,
date TEXT NOT NULL, -- YYYY-MM-DD
summary TEXT NOT NULL,
mood TEXT,
topics TEXT, -- JSON array
observations TEXT, -- JSON array
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
observationstable:
CREATE TABLE observations (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
confidence REAL DEFAULT 0.5,
category TEXT, -- 'topic_interest', 'interaction_style', 'mood_pattern'
applied INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
- Connect scheduler's nightly diary trigger to LLM-generated summaries (currently writes stub text)
- Reflection → Personality update pipeline:
- Diary entries accumulate → periodic analysis → trait drift candidates
- Approval process: observations → candidates → bounded update
- Max 0.5% trait change/day, max 5%/month — slow evolution feels human, fast feels schizophrenic
- Test: Simulate conversations across multiple "days", verify diary entries, verify trait drift remains within bounds
Phase 6 — MCP Tools & Integrations
Goal: Extensible tool system for web search, file access, external services.
Status: PARTIAL
internal/tools/registry.go— register/list/execute tools with parameter definitionsinternal/tools/builtin.go—web_fetch(working),web_search(stub)- MCP protocol client (
internal/tools/mcp.go):- Connect to MCP servers via stdio or TCP
- List available tools with parameters
- Execute tool calls, parse results into context
- Tool call loop in agent:
- LLM decides to call tool → agent executes → result appended to context → LLM generates final response
- MCP-managed integrations:
- Obsidian vault (via
enquire-mcpor custom) - Gitea/Forgejo operations
- Actual Budget (spending tracking, categorization)
- Obsidian vault (via
- Test:
web_fetchtool, verify agent can search, summarize, cite sources
Phase 7 — Matrix Integration
Goal: Agent lives as a Matrix bot, accessible via DM. Matrix feels like @diva:homeserver.tld instead of Bot #4321.
Status: STUB
internal/channel/matrix.go— config + start stub (disabled by default)- Full Matrix bot with
mautrix-go:- Listen for invitations, join rooms
- Handle DMs with typing indicators, read receipts
- Message parsing & sending
- Bot identity:
@diva:homeserver.tld
- Config: homeserver URL, bot user token in env
- TUI and Matrix run concurrently — messages sync between both
- Test: Send DM to bot, verify response, verify cross-session memory
Phase 8 — Self-Memory & Identity
Goal: Agent remembers its own state — promises, strategies, mistakes. "Bidirectional memory."
Status: CODE DONE
internal/memory/self_memory.go:
type Promise struct {
ID string
Promise string
Context string
Fulfilled bool
CreatedAt time.Time
Deadline *time.Time
}
type Strategy struct {
ID string
Situation string
Strategy string
Outcome string
CreatedAt time.Time
}
agent_promisestable (id, promise, context, fulfilled, created_at, deadline)agent_strategiestable (id, situation, strategy, outcome, created_at)- Wire into Prompt Composer: recent promises, relevant strategies added to context
- Test: "What did you promise me yesterday?" → verify recall from self-memory
Phase 9 — Safety Guardrails & Identity Stability
Goal: Prevent personality drift, maintain core safety rules.
internal/companion/safety.go:- Core Identity (never changes): values, communication style, boundaries, role definition
- State (dynamic): mood, interests, current projects, relationship opinions
- Separation: identity config (env/read-only) vs state (SQLite/writable)
- Personality safety rules:
- Traits bounded within configured min/max at all times
- Core safety rules override any personality state
- Permission policies cannot be modified by personality
- No tool access changes via personality evolution
- Prompt injection protection:
- User messages wrapped in separator tokens
- System prompt templates with strict, non-overridable sections
- Input sanitization at the agent boundary
- Test: Attempt prompt injection, verify guardrails hold
Phase 10 — Polish: Streaming TUI, Token Tracking, Observability
Goal: Production-quality UX and operations.
- Token streaming in TUI — char-by-char from llama.cpp SSE or chunked response
- Context window manager:
- Track total token count per conversation
- Sliding window when context exceeds limit
- Auto-summarize oldest messages to stay within context
POST /v1/chatstreaming via SSE in API mode- Structured JSON logging via
log/slog(already done — custom handler with[LEVEL] [TIME] [FILE:LINE]) - Prometheus metrics (
GET /metrics) - Graceful shutdown — drain in-flight requests, save agent state
- Test: Full restart cycle, verify all state restored
Phase 11 — Future Enhancements (Optional)
- Voice interface: whisper.cpp (STT) + piper/XTTS (TTS)
- VRM/Live2D avatar rendering (like Open-LLM-VTuber, Soul of Waifu)
- Multi-user support with per-user relationship tracking
- Multi-room Matrix support
- Plugin system for community-contributed tools
- WebUI dashboard (like nanobot's WebUI)
- Mobile companion via Matrix (any Matrix client works)
- Home Assistant integration for physical automation
- PostgreSQL + pgvector migration for production-scale vector search
Database Summary
Two separate SQLite databases (clean separation of concerns):
| Database | Purpose | Tables |
|---|---|---|
divacode.db |
Core agent data | conversations, messages, context_state |
divacode.db |
Memory | memories |
companion.db |
Personality | personality_traits |
companion.db |
Relationship | relationship_metrics, shared_topics, inside_jokes |
companion.db |
Reflection | diary_entries, observations |
companion.db |
Self-memory | agent_promises, agent_strategies |
Future: divacode.db may be replaced by PostgreSQL + pgvector for production deployments with many users or large memory stores.
Key Principles
- Build the smallest loop first — get message → memory → LLM → response → diary → reflection working before adding complexity
- Personality is data, not prompt text — traits stored in SQLite, composed dynamically by the prompt builder
- Memory ≠ Personality — factual memory (Engram-style RAG) is separate from relationship/emotional state
- Slow evolution — max 0.5% trait change/day, max 5%/month. Fast evolution feels schizophrenic, slow feels human
- Core Identity is stable — identity.yaml (or env config) never changes much; state.json (SQLite) changes constantly
- Diary before reflection — the nightly diary is the emotional continuity layer, more important than vector search
- Autonomous messaging — the scheduler is what makes the character feel alive, not the LLM intelligence
- No secrets in code — all config via env vars,
.envin.gitignore - No config libraries — std lib
os.Getenvonly, nevercaarlos0/envor similar - No HTTP routers — std lib
net/httponly, no third-party routers (chi, gorilla, etc.) - Error handling — wrap errors with
fmt.Errorf("context: %w", err), never silent discards - Test as you go —
go test -race ./...before every push - Output readable code — explain why, never what. No comments for self-documenting code