Phase 1 of the Companion Brain architecture. Introduces a typed, synchronous event bus with microtask-scheduled handlers, an append-only event store persisted to IndexedDB (_events table, v10 schema), and semantic domain events emitted from module stores. Pilot modules with emit() calls: - Todo: TaskCreated, TaskCompleted, TaskUncompleted, TaskDeleted, SubtasksUpdated - Calendar: CalendarEventCreated, CalendarEventUpdated, CalendarEventDeleted - Drink: DrinkLogged, DrinkEntryDeleted, DrinkEntryUndone - Nutriphi: MealLogged, MealFromPhotoLogged, MealDeleted - Places: PlaceCreated, PlaceDeleted, PlaceVisited, LocationLogged, TrackingStarted, TrackingStopped Also includes the full architecture plan at docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
42 KiB
Mana Companion Brain — Architecture & Implementation Plan
Vollstaendiger Umbau-Plan fuer ein zentrales Intelligenz-System ueber alle Module. Start mit 5 Pilot-Modulen: Todo, Calendar, Drink, Nutriphi, Places. Stand: April 2026
1. Vision
Mana hat 40+ Module, die isoliert arbeiten. Der Companion Brain verbindet sie zu einem System, das den Nutzer proaktiv begleitet — erinnert, motiviert, Muster erkennt und Zusammenhaenge zwischen Modulen herstellt. Alles lokal, privacy-first.
Drei Saeulen:
- Pulse — Regelbasierte Nudges & Tageszusammenfassungen (kein LLM)
- Rituale — Gefuehrte Routinen die Daten in Module schreiben (AI-generiert)
- Companion Chat — LLM mit Tool-Zugriff auf alle Module
Fundament:
- Domain Event Bus (semantische Events statt CRUD-Logs)
- Projection Engine (live-reaktive Aggregation ueber alle Module)
- Goal System (moduluebergreifende Ziele mit Fortschritt)
- Semantic Memory (extrahiertes Nutzerwissen, persistent)
- Tool Layer (standardisierter LLM-Zugriff auf Module)
- Feedback Loop (Nudge-Outcomes fuer Lernfaehigkeit)
2. Architektur-Uebersicht
+---------------------------------------------------+
| MODULE LAYER |
| Todo - Calendar - Drink - Nutriphi - Places |
| Jedes Modul emittiert Domain Events via Stores |
+------------------------+--------------------------+
| emit()
v
+---------------------------------------------------+
| EVENT BUS |
| Typed, synchron, in-process |
| TaskCompleted - DrinkLogged - EventCreated ... |
+--+--------+--------+--------+--------+-----------+
| | | | |
v v v v v
+------+ +------+ +------+ +------+ +------+
|Event | |State | |Proj. | |Rule | |Trig- |
|Store | |Write | |Engine| |Engine| |gers |
| | |Dexie | | | | | | |
+------+ +------+ +--+---+ +--+---+ +------+
| |
+----------+--------+----------+
v v v v
+---------------------------------------------------+
| INTELLIGENCE LAYER |
| |
| +------------+ +----------+ +-------+ +--------+ |
| |Projections | | Memory | | Goals | |Feedback| |
| |DaySnapshot | | Patterns | | Meter | | Loop | |
| |Streaks | | Prefs | | Track | | Nudge | |
| |Correlations| | Context | | Link | | Outcome| |
| +-----+------+ +----+-----+ +---+---+ +---+----+ |
| | | | | |
| +--------------+-----------+---------+ |
| v |
| Context Document Generator |
| (~500 Token Nutzer-Snapshot) |
+------------------------+--------------------------+
|
v
+---------------------------------------------------+
| INTERACTION LAYER |
| |
| +----------+ +----------+ +---------+ +---------+ |
| | Pulse | | Rituale | |Companion| |Insights | |
| | Engine | | (AI-gen) | | Chat | | Cards | |
| | regelb. | | | |LLM+Tool | | | |
| +----------+ +----------+ +---------+ +---------+ |
| |
| Feedback: Nudge -> Outcome -> Memory Update |
+---------------------------------------------------+
3. Domain Event System
3.1 Warum Domain Events statt CRUD-Logs
Aktuell loggt _activity nur { op: 'update', collection: 'tasks', recordId }. Daraus laesst sich nicht ableiten, was passiert ist. Wurde der Task erledigt? Umbenannt? Verschoben? Das erzwingt Archaeologie — Felder vergleichen, Semantik raten.
Domain Events tragen Bedeutung: TaskCompleted { taskId, title, project } ist sofort verstaendlich fuer Projections, Rules, LLM und Mensch.
3.2 Event Bus Interface
Neues File: apps/mana/apps/web/src/lib/data/events/event-bus.ts
// ── Core Types ──────────────────────────────────────
export interface DomainEvent<T extends string = string, P = unknown> {
type: T;
payload: P;
meta: EventMeta;
}
export interface EventMeta {
id: string; // crypto.randomUUID()
timestamp: string; // ISO
appId: string; // source module
collection: string; // source table
recordId: string; // affected record
userId: string; // from getEffectiveUserId()
causedBy?: string; // parent event id (for trigger chains)
}
// ── Bus Interface ───────────────────────────────────
export type EventHandler<E extends DomainEvent = DomainEvent> = (event: E) => void;
export interface EventBus {
emit(event: DomainEvent): void;
on<T extends string>(type: T, handler: EventHandler): () => void;
onAny(handler: EventHandler): () => void;
off(type: string, handler: EventHandler): void;
}
Implementierung: Einfacher synchroner Dispatcher mit async Subscribers.
emit()ist synchron (blockiert Dexie-Hook nicht)- Handlers laufen in
queueMicrotask()— nach dem Hook, aber vor dem naechsten Frame - Guard gegen Endlos-Loops:
_emittingSet verhindert re-entrant emits vom selben Event-Typ
export function createEventBus(): EventBus {
const handlers = new Map<string, Set<EventHandler>>();
const anyHandlers = new Set<EventHandler>();
return {
emit(event: DomainEvent) {
queueMicrotask(() => {
const typeHandlers = handlers.get(event.type);
if (typeHandlers) {
for (const h of typeHandlers) h(event);
}
for (const h of anyHandlers) h(event);
});
},
on(type, handler) {
if (!handlers.has(type)) handlers.set(type, new Set());
handlers.get(type)!.add(handler);
return () => handlers.get(type)?.delete(handler);
},
onAny(handler) {
anyHandlers.add(handler);
return () => anyHandlers.delete(handler);
},
off(type, handler) {
handlers.get(type)?.delete(handler);
},
};
}
// Singleton
export const eventBus = createEventBus();
3.3 Event Store
Ersetzt die _activity-Tabelle als primaere Quelle fuer "was ist passiert".
Neue Dexie-Tabelle _events:
_events: '++seq, meta.id, meta.type, meta.appId, meta.timestamp,
[meta.appId+meta.timestamp], [meta.type+meta.timestamp]'
Felder:
seq— Auto-increment (Reihenfolge-Garantie)type— Domain Event Type (z.B. 'TaskCompleted')payload— Serialisiertes Event-Payload (verschluesselt fuer sensitive Felder)meta— EventMeta Objekt
Retention: 90 Tage (wie _activity), max 50.000 Events. Pruning via bestehender Quota-Recovery.
Subscriber: eventBus.onAny() schreibt jedes Event in _events.
3.4 Domain Events pro Modul (5 Pilot-Module)
Todo Events
| Event | Payload | Abgeleitet aus |
|---|---|---|
TaskCreated |
{ taskId, title, dueDate?, priority?, projectId?, labelIds? } |
tasksStore.createTask() |
TaskCompleted |
{ taskId, title, projectId?, wasOverdue: boolean } |
tasksStore.completeTask() |
TaskUncompleted |
{ taskId, title } |
tasksStore.uncompleteTask() |
TaskUpdated |
{ taskId, fields: string[] } |
tasksStore.updateTask() |
TaskDeleted |
{ taskId, title } |
tasksStore.deleteTask() |
TaskRescheduled |
{ taskId, title, oldDate?, newDate } |
updateTask wenn dueDate aendert |
SubtasksUpdated |
{ taskId, total, completed } |
tasksStore.updateSubtasks() |
ReminderSet |
{ taskId, minutesBefore, type } |
remindersStore.createReminder() |
Calendar Events
| Event | Payload | Abgeleitet aus |
|---|---|---|
CalendarEventCreated |
{ eventId, title, startTime, endTime, isAllDay, isRecurring, calendarId } |
eventsStore.createEvent() |
CalendarEventUpdated |
{ eventId, fields: string[] } |
eventsStore.updateEvent() |
CalendarEventDeleted |
{ eventId, title, wasRecurring } |
eventsStore.deleteEvent() |
CalendarEventMoved |
{ eventId, title, oldStart, newStart } |
updateEvent wenn startTime aendert |
Drink Events
| Event | Payload | Abgeleitet aus |
|---|---|---|
DrinkLogged |
{ entryId, drinkType, quantityMl, name, date, time, fromPreset: boolean } |
drinkStore.logDrink(), logFromPreset() |
DrinkEntryDeleted |
{ entryId, drinkType, quantityMl } |
drinkStore.deleteEntry() |
DrinkEntryUndone |
{ entryId } |
drinkStore.undoLastEntry() |
DrinkGoalReached |
{ date, goalMl, actualMl, drinkType: 'water' } |
Projection erkennt Zielerreichung |
Nutriphi Events
| Event | Payload | Abgeleitet aus |
|---|---|---|
MealLogged |
{ mealId, mealType, inputType, description, calories?, protein?, date } |
mealMutations.create() |
MealFromPhotoLogged |
{ mealId, mealType, photoMediaId, confidence, foods? } |
mealMutations.createFromPhoto() |
MealDeleted |
{ mealId, mealType } |
mealMutations.delete() |
NutritionGoalSet |
{ dailyCalories, dailyProtein?, dailyCarbs?, dailyFat? } |
goalMutations.create/update() |
DailyCalorieGoalReached |
{ date, goal, actual } |
Projection erkennt Zielerreichung |
Places Events
| Event | Payload | Abgeleitet aus |
|---|---|---|
PlaceCreated |
{ placeId, name, category?, lat, lng } |
placesStore.createPlace() |
PlaceVisited |
{ placeId, name, visitCount } |
placesStore.recordVisit() |
LocationLogged |
{ logId, lat, lng, placeId?, accuracy } |
trackingStore.logNow() |
TrackingStarted |
{} |
trackingStore.startTracking() |
TrackingStopped |
{ durationMs, logCount } |
trackingStore.stopTracking() |
3.5 Event-Emission aus Module Stores
Jeder Store bekommt emit()-Calls in seinen Mutations. Kein Umbau der Dexie-Hooks noetig — Events werden im Store emittiert, nicht im Hook.
Warum im Store statt im Hook? Der Hook sieht nur CRUD. Der Store kennt die Semantik. completeTask() weiss, dass es ein Completion ist — der Hook sieht nur update({ completedAt }).
Beispiel: Todo Store nach Umbau:
// stores/tasks.svelte.ts
import { eventBus } from '$lib/data/events/event-bus';
export const tasksStore = {
async completeTask(id: string) {
const task = await taskTable.get(id);
if (!task) return;
const now = new Date().toISOString();
const wasOverdue = task.dueDate && task.dueDate < now.slice(0, 10);
await taskTable.update(id, { completedAt: now, updatedAt: now });
eventBus.emit({
type: 'TaskCompleted',
payload: {
taskId: id,
title: task.title, // plaintext snapshot (pre-encryption)
projectId: task.projectId,
wasOverdue,
},
meta: {
id: crypto.randomUUID(),
timestamp: now,
appId: 'todo',
collection: 'tasks',
recordId: id,
userId: getEffectiveUserId(),
},
});
},
// ... andere Mutations analog
};
Konvention: Jede Store-Mutation die einen Seiteneffekt hat, emittiert ein Event. Reine UI-State-Aenderungen (z.B. calendarViewStore.setDate()) emittieren nicht.
3.6 Event Helper fuer Module
Um Boilerplate zu reduzieren, ein createEventEmitter Helper:
Neues File: apps/mana/apps/web/src/lib/data/events/emit.ts
import { eventBus } from './event-bus';
import { getEffectiveUserId } from '../current-user';
export function emitDomainEvent<P>(
type: string,
appId: string,
collection: string,
recordId: string,
payload: P,
causedBy?: string
): void {
eventBus.emit({
type,
payload,
meta: {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
appId,
collection,
recordId,
userId: getEffectiveUserId(),
causedBy,
},
});
}
Aufruf im Store wird dann einzeilig:
emitDomainEvent('TaskCompleted', 'todo', 'tasks', id, {
taskId: id, title: task.title, projectId: task.projectId, wasOverdue,
});
4. Projection Engine
4.1 Prinzip
Projections sind live-reaktive Aggregationen ueber Modul-Daten. Sie hoeren Domain Events und aktualisieren sich inkrementell. Consumers (Pulse, Companion, Dashboard) lesen Projections — nie Rohdaten.
Neuer Ordner: apps/mana/apps/web/src/lib/data/projections/
4.2 DaySnapshot
Beantwortet: "Was ist heute los?"
// projections/day-snapshot.ts
export interface DaySnapshot {
date: string; // YYYY-MM-DD
// Todo
tasks: {
total: number;
completed: number;
overdue: number;
dueToday: TaskSummary[];
};
// Calendar
events: {
upcoming: EventSummary[]; // naechste 5 Events
total: number;
nextEvent?: EventSummary;
};
// Drink
drinks: {
water: { ml: number; goal: number; percent: number };
coffee: { ml: number; count: number };
other: { ml: number; count: number };
total: { ml: number; count: number };
};
// Nutriphi
nutrition: {
meals: number;
calories: { actual: number; goal: number; percent: number };
protein?: { actual: number; goal: number };
};
// Places
places: {
visited: number;
currentLocation?: { lat: number; lng: number; placeName?: string };
tracking: boolean;
};
}
Implementierung: Dexie liveQueries die auf $derived gemapped werden. Event-Listener fuer inkrementelle Updates (z.B. DrinkLogged addiert direkt statt neu zu querien).
4.3 Streaks
Beantwortet: "Was laeuft gut, was droht zu brechen?"
// projections/streaks.ts
export interface StreakInfo {
moduleId: string;
label: string; // "Wasser-Ziel", "Journal", "Sport"
currentStreak: number; // Tage in Folge
longestStreak: number;
lastActiveDate: string; // YYYY-MM-DD
status: 'active' | 'at_risk' | 'broken';
// active: heute oder gestern aktiv
// at_risk: gestern nicht aktiv, vorgestern schon
// broken: >1 Tag Pause
}
Berechnet aus: TimeBlocks + Modul-spezifische Logik (Drink: Tagesziel erreicht, Todo: mindestens 1 Task erledigt, etc.)
4.4 Correlations
Beantwortet: "Was haengt zusammen?"
// projections/correlations.ts
export interface Correlation {
id: string;
factorA: { module: string; metric: string; label: string };
factorB: { module: string; metric: string; label: string };
coefficient: number; // Pearson r, -1 bis +1
pValue: number; // Statistische Signifikanz
sampleSize: number; // Anzahl Tage
direction: 'positive' | 'negative';
sentence: string; // "An Tagen mit Sport trinkst du 30% mehr Wasser"
computedAt: string;
}
Berechnung: 1x taeglich, ueber TimeBlocks + Tagesaggregate der letzten 30-90 Tage. Pearson-Korrelation zwischen Paaren. Nur Korrelationen mit |r| > 0.3 und p < 0.05 werden gespeichert.
Metriken pro Modul:
- Todo: tasks_completed_count, overdue_count
- Calendar: events_count, meeting_hours
- Drink: water_ml, coffee_count, goal_reached (boolean)
- Nutriphi: calories, protein, meals_count
- Places: places_visited, distance_km
4.5 ContactHealth (spaeter, nicht in Pilot)
Wird mit dem Contacts-Modul relevant. Trackt Kontakthaeufigkeit vs. erwartete Frequenz.
5. Goal System
5.1 Datenmodell
Neue Dexie-Tabelle: goals
goals: 'id, moduleId, status, [moduleId+status]'
export interface LocalGoal {
id: string;
title: string; // "4x Sport pro Woche"
description?: string;
// Metrik-Definition
metric: GoalMetric;
target: GoalTarget;
// Verknuepfung
moduleId: string; // primaeres Modul
linkedModules: string[]; // weitere beteiligte Module
// Status
status: 'active' | 'paused' | 'completed' | 'abandoned';
// Tracking
currentValue: number; // live berechnet
currentPeriodStart: string; // Beginn der aktuellen Periode
history: GoalPeriodResult[]; // vergangene Perioden
createdAt: string;
updatedAt: string;
deletedAt?: string;
}
export interface GoalMetric {
source: 'event_count' | 'event_sum' | 'streak_days' | 'custom';
eventType?: string; // Domain Event Type (z.B. 'DrinkLogged')
filterField?: string; // z.B. 'drinkType'
filterValue?: string; // z.B. 'water'
sumField?: string; // z.B. 'quantityMl' (fuer event_sum)
}
export interface GoalTarget {
value: number; // Zielwert
period: 'day' | 'week' | 'month';
comparison: 'gte' | 'lte' | 'eq'; // >= (Sport), <= (Kaffee), = (exakt)
}
export interface GoalPeriodResult {
periodStart: string;
periodEnd: string;
value: number;
reached: boolean;
}
5.2 Goal-Tracking via Events
Der Goal-Tracker subscribed auf den Event Bus und aktualisiert currentValue inkrementell:
// Beispiel: Ziel "8 Glaeser Wasser/Tag"
// metric: { source: 'event_count', eventType: 'DrinkLogged', filterField: 'drinkType', filterValue: 'water' }
// target: { value: 8, period: 'day', comparison: 'gte' }
eventBus.on('DrinkLogged', (event) => {
if (event.payload.drinkType === 'water') {
goal.currentValue += 1;
if (goal.currentValue >= goal.target.value) {
emitDomainEvent('GoalReached', 'companion', 'goals', goal.id, {
goalId: goal.id, title: goal.title, value: goal.currentValue,
});
}
}
});
5.3 Vordefinierte Ziel-Templates
Fuer den Start 10-15 Templates die der Nutzer mit einem Tap aktiviert:
- 8 Glaeser Wasser/Tag (Drink, event_count, DrinkLogged, water)
- 2000 kcal/Tag (Nutriphi, event_sum, MealLogged, calories)
- 5 Tasks/Tag erledigen (Todo, event_count, TaskCompleted)
- Alle Mahlzeiten tracken (Nutriphi, event_count, MealLogged, 3/day)
- Jeden Tag einen neuen Ort besuchen (Places, event_count, PlaceVisited, 1/day)
6. Semantic Memory
6.1 Datenmodell
Neue Dexie-Tabelle: _memory
_memory: 'id, category, confidence, lastConfirmed, [category+confidence]'
export interface MemoryFact {
id: string;
category: 'pattern' | 'preference' | 'relationship' | 'context';
content: string; // Menschenlesbarer Fakt
// "Trainiert typischerweise Mo/Mi/Fr abends"
// "Trinkt morgens immer zuerst Kaffee, dann Wasser"
// "Meetings haeufig Di/Do vormittags"
confidence: number; // 0.0 - 1.0
confirmations: number; // wie oft bestaetigt
contradictions: number; // wie oft widersprochen
sourceEvents: string[]; // Event-IDs die diesen Fakt stuetzen
sourceModules: string[]; // beteiligte Module
firstSeen: string; // wann erstmals erkannt
lastConfirmed: string; // letzte Bestaetigung
expiresAt?: string; // fuer temporaere Kontexte
createdAt: string;
updatedAt: string;
deletedAt?: string;
}
6.2 Extraktion
Zwei Wege:
-
Regelbasiert (kein LLM): Pattern-Detektoren ueber Event-Stream:
- Wiederholungs-Detektor: "3x in 2 Wochen am Montag trainiert → Pattern: trainiert montags"
- Zeitfenster-Detektor: "Tasks werden zu 80% zwischen 09-12 Uhr erledigt → Preference: Morgen-Produktivitaet"
- Sequenz-Detektor: "Kaffee wird immer vor dem ersten Event geloggt → Pattern: Kaffee vor Meetings"
-
LLM-basiert (Tier 1 browser): Woechentlich zusammengefasste Events an lokales Gemma-Modell:
- "Hier sind die Events der letzten Woche. Welche Muster und Praeferenzen erkennst du?"
- Ergebnis als JSON parsen → MemoryFact[] schreiben
6.3 Confidence-Lifecycle
Neuer Fakt erkannt → confidence: 0.3
Nochmal bestaetigt → confidence: 0.5
3+ Bestaetigungen → confidence: 0.7
10+ Bestaetigungen → confidence: 0.9
Widerspruch erkannt → confidence -= 0.15
Laenger als 30 Tage nicht → confidence -= 0.05/Woche
bestaetigt
confidence < 0.1 → Fakt wird geloescht
7. Context Document Generator
7.1 Zweck
Komprimiert den gesamten Nutzerzustand in ein ~500 Token Dokument, das als System-Prompt an das LLM geht. Aktualisiert sich bei jedem Companion-Aufruf.
7.2 Template
// projections/context-document.ts
export function generateContextDocument(
day: DaySnapshot,
streaks: StreakInfo[],
goals: LocalGoal[],
memory: MemoryFact[],
correlations: Correlation[]
): string {
return `## Nutzer-Kontext (${day.date})
### Heute
- ${day.tasks.dueToday.length} Tasks faellig (${day.tasks.completed} erledigt, ${day.tasks.overdue} ueberfaellig)
- ${day.events.total} Termine${day.events.nextEvent ? ` — naechster: ${day.events.nextEvent.title} um ${day.events.nextEvent.startTime}` : ''}
- Wasser: ${day.drinks.water.ml}ml von ${day.drinks.water.goal}ml (${day.drinks.water.percent}%)
- Ernaehrung: ${day.nutrition.calories.actual} von ${day.nutrition.calories.goal} kcal, ${day.nutrition.meals} Mahlzeiten
${day.places.tracking ? `- Standort-Tracking aktiv` : ''}
### Ziele
${goals.filter(g => g.status === 'active').map(g =>
`- "${g.title}" — ${g.currentValue}/${g.target.value} (${g.target.period})`
).join('\n')}
### Streaks
${streaks.filter(s => s.status !== 'broken').map(s =>
`- ${s.label}: ${s.currentStreak} Tage${s.status === 'at_risk' ? ' (GEFAEHRDET)' : ''}`
).join('\n')}
${streaks.filter(s => s.status === 'broken').map(s =>
`- ${s.label}: UNTERBROCHEN seit ${daysSince(s.lastActiveDate)} Tagen`
).join('\n')}
### Bekannte Muster
${memory.filter(m => m.confidence > 0.5).map(m => `- ${m.content}`).join('\n')}
### Korrelationen
${correlations.slice(0, 3).map(c => `- ${c.sentence}`).join('\n')}
`;
}
8. Tool Layer (LLM Write-Access)
8.1 ModuleTool Interface
Neues File: apps/mana/apps/web/src/lib/data/tools/types.ts
export interface ModuleTool {
name: string; // 'create_task', 'log_drink'
module: string; // 'todo', 'drink'
description: string; // Fuer LLM Function-Schema
parameters: ToolParameter[];
execute: (params: Record<string, unknown>) => Promise<ToolResult>;
}
export interface ToolParameter {
name: string;
type: 'string' | 'number' | 'boolean';
description: string;
required: boolean;
enum?: string[]; // z.B. ['water', 'coffee', 'tea']
}
export interface ToolResult {
success: boolean;
data?: unknown;
message?: string; // Menschenlesbare Bestaetigung
}
8.2 Tool-Definitionen (5 Pilot-Module)
Jedes Modul bekommt eine tools.ts:
// modules/todo/tools.ts
export const todoTools: ModuleTool[] = [
{
name: 'create_task',
module: 'todo',
description: 'Erstellt einen neuen Task',
parameters: [
{ name: 'title', type: 'string', description: 'Titel des Tasks', required: true },
{ name: 'dueDate', type: 'string', description: 'Faelligkeitsdatum (YYYY-MM-DD)', required: false },
{ name: 'priority', type: 'number', description: 'Prioritaet 0-3', required: false },
],
execute: async (params) => {
const task = await tasksStore.createTask({
title: params.title as string,
dueDate: params.dueDate as string | undefined,
priority: params.priority as number | undefined,
});
return { success: true, data: task, message: `Task "${task.title}" erstellt` };
},
},
{
name: 'complete_task',
module: 'todo',
description: 'Markiert einen Task als erledigt',
parameters: [
{ name: 'taskId', type: 'string', description: 'ID des Tasks', required: true },
],
execute: async (params) => {
await tasksStore.completeTask(params.taskId as string);
return { success: true, message: 'Task erledigt' };
},
},
];
// modules/drink/tools.ts
export const drinkTools: ModuleTool[] = [
{
name: 'log_drink',
module: 'drink',
description: 'Loggt ein Getraenk',
parameters: [
{ name: 'drinkType', type: 'string', description: 'Art', required: true, enum: ['water', 'coffee', 'tea', 'juice', 'alcohol', 'other'] },
{ name: 'quantityMl', type: 'number', description: 'Menge in ml', required: true },
{ name: 'name', type: 'string', description: 'Name des Getraenks', required: false },
],
execute: async (params) => {
const entry = await drinkStore.logDrink({
name: (params.name as string) ?? (params.drinkType as string),
drinkType: params.drinkType as DrinkType,
quantityMl: params.quantityMl as number,
});
return { success: true, data: entry, message: `${params.quantityMl}ml ${params.drinkType} geloggt` };
},
},
];
// modules/calendar/tools.ts — create_event
// modules/nutriphi/tools.ts — log_meal
// modules/places/tools.ts — record_visit, create_place
8.3 Tool Registry
Neues File: apps/mana/apps/web/src/lib/data/tools/registry.ts
import { todoTools } from '$lib/modules/todo/tools';
import { calendarTools } from '$lib/modules/calendar/tools';
import { drinkTools } from '$lib/modules/drink/tools';
import { nutriphiTools } from '$lib/modules/nutriphi/tools';
import { placesTools } from '$lib/modules/places/tools';
const ALL_TOOLS: ModuleTool[] = [
...todoTools,
...calendarTools,
...drinkTools,
...nutriphiTools,
...placesTools,
];
export function getTools(): ModuleTool[] {
return ALL_TOOLS;
}
export function getTool(name: string): ModuleTool | undefined {
return ALL_TOOLS.find((t) => t.name === name);
}
export function getToolsForLlm(): LlmFunctionSchema[] {
return ALL_TOOLS.map((t) => ({
name: t.name,
description: t.description,
parameters: {
type: 'object',
properties: Object.fromEntries(
t.parameters.map((p) => [p.name, {
type: p.type,
description: p.description,
...(p.enum ? { enum: p.enum } : {}),
}])
),
required: t.parameters.filter((p) => p.required).map((p) => p.name),
},
}));
}
8.4 Integration mit LLM Orchestrator
Der bestehende LlmOrchestrator in @mana/shared-llm bekommt eine neue Methode:
// In shared-llm/src/orchestrator.ts
async runWithTools<TOut>(
task: LlmTask,
input: { messages: ChatMessage[]; tools: LlmFunctionSchema[] },
): Promise<LlmTaskResult<TOut>>
Das LLM gibt tool_use Responses zurueck, die der Orchestrator ueber getTool(name).execute(params) ausfuehrt. Das Ergebnis wird als tool_result Message zurueckgefuettert.
9. Rule Engine (Pulse)
9.1 Prinzip
Die Rule Engine liest Projections und erzeugt Nudges. Kein LLM — rein deterministisch. Laeuft auf zwei Wegen:
- Event-getriggert: Reagiert auf Domain Events (z.B.
TaskCompleted→ Streak-Check) - Zeitgesteuert: Periodische Checks (Morgen-Summary, Abend-Reflexion, stuendlich)
9.2 Rule Interface
Neues File: apps/mana/apps/web/src/lib/companion/rules/types.ts
export interface PulseRule {
id: string;
name: string;
description: string;
// Trigger
trigger:
| { kind: 'event'; eventType: string }
| { kind: 'schedule'; cron: string } // z.B. '0 8 * * *' (08:00 taeglich)
| { kind: 'interval'; minutes: number }; // z.B. 60 (stuendlich)
// Check — gibt null zurueck wenn kein Nudge noetig
check: (ctx: RuleContext) => Promise<Nudge | null>;
}
export interface RuleContext {
day: DaySnapshot;
streaks: StreakInfo[];
goals: LocalGoal[];
memory: MemoryFact[];
now: Date;
}
export interface Nudge {
id: string;
type: NudgeType;
title: string;
body: string;
priority: 'low' | 'medium' | 'high';
actionLabel?: string; // "Jetzt loggen"
actionRoute?: string; // '/drink'
actionTool?: string; // 'log_drink' — Companion kann direkt ausfuehren
expiresAt?: string; // wann der Nudge irrelevant wird
}
type NudgeType =
| 'streak_warning'
| 'goal_progress'
| 'goal_reached'
| 'morning_summary'
| 'evening_reflection'
| 'overdue_tasks'
| 'water_reminder'
| 'meal_reminder'
| 'correlation_insight';
9.3 Vordefinierte Rules (Pilot)
// rules/water-reminder.ts
export const waterReminderRule: PulseRule = {
id: 'water-reminder',
name: 'Wasser-Erinnerung',
trigger: { kind: 'interval', minutes: 90 },
async check(ctx) {
const { water } = ctx.day.drinks;
if (water.percent >= 100) return null; // Ziel erreicht
const hourOfDay = ctx.now.getHours();
if (hourOfDay < 8 || hourOfDay > 21) return null; // Nachtruhe
const remaining = water.goal - water.ml;
const hoursLeft = 21 - hourOfDay;
const mlPerHour = Math.ceil(remaining / hoursLeft);
return {
id: `water-${ctx.day.date}-${hourOfDay}`,
type: 'water_reminder',
title: 'Wasser trinken',
body: `Noch ${remaining}ml bis zum Ziel. ~${mlPerHour}ml pro Stunde.`,
priority: water.percent < 50 ? 'medium' : 'low',
actionLabel: 'Glas loggen',
actionTool: 'log_drink',
};
},
};
// rules/streak-warning.ts
export const streakWarningRule: PulseRule = {
id: 'streak-warning',
name: 'Streak-Warnung',
trigger: { kind: 'schedule', cron: '0 18 * * *' }, // 18:00 taeglich
async check(ctx) {
const atRisk = ctx.streaks.filter(s => s.status === 'at_risk');
if (atRisk.length === 0) return null;
const best = atRisk.reduce((a, b) => a.currentStreak > b.currentStreak ? a : b);
return {
id: `streak-${ctx.day.date}`,
type: 'streak_warning',
title: `${best.label}-Streak in Gefahr!`,
body: `${best.currentStreak} Tage — nicht heute brechen.`,
priority: best.currentStreak > 7 ? 'high' : 'medium',
};
},
};
// rules/morning-summary.ts
// rules/evening-reflection.ts
// rules/overdue-tasks.ts
// rules/meal-reminder.ts
// rules/goal-reached.ts
9.4 Rule Engine Runner
Integriert sich in den bestehenden Reminder-Scheduler als zusaetzliche Source:
// companion/rules/engine.ts
export function createRuleEngine(rules: PulseRule[]): ReminderSource {
return {
id: 'companion-pulse',
async checkDue(): Promise<DueReminder[]> {
const ctx = await buildRuleContext();
const nudges: Nudge[] = [];
for (const rule of rules) {
if (shouldTrigger(rule)) {
const nudge = await rule.check(ctx);
if (nudge && !isDismissed(nudge.id)) {
nudges.push(nudge);
}
}
}
return nudges.map(toReminder);
},
async markSent(id) { /* track in localStorage */ },
};
}
10. Feedback Loop
10.1 Datenmodell
Neue Dexie-Tabelle: _nudgeOutcomes
_nudgeOutcomes: '++id, nudgeId, nudgeType, outcome, timestamp, [nudgeType+outcome]'
export interface NudgeOutcome {
id?: number;
nudgeId: string;
nudgeType: NudgeType;
outcome: 'acted' | 'dismissed' | 'snoozed' | 'expired' | 'auto_resolved';
latencyMs?: number; // Zeit bis Reaktion
timestamp: string;
}
10.2 Lerneffekt
Aggregation ueber _nudgeOutcomes beeinflusst die Rule Engine:
// Beispiel: Wasser-Reminder wird um 10:00 immer dismissed
// → confidence fuer "Nutzer mag morgens keine Wasser-Reminder" steigt
// → Rule Engine verschiebt auf 11:00
// Beispiel: Streak-Warning um 18:00 fuehrt zu 80% zu Aktion
// → confidence fuer "18:00 ist guter Zeitpunkt" steigt
// → bleibt bei 18:00
Memory-Facts werden aus Outcome-Patterns extrahiert und fliessen in den Context Document Generator.
11. Companion Chat (Interaction Layer)
11.1 Modul-Struktur
Neues Modul: apps/mana/apps/web/src/lib/modules/companion/
companion/
module.config.ts — Registriert companion-Tabellen
collections.ts — conversations, messages, rituals, goals
stores/
chat.svelte.ts — Chat-Mutations (send, receive, tool-call)
rituals.svelte.ts — Ritual-CRUD + Step-Execution
goals.svelte.ts — Goal-CRUD + Progress-Tracking
queries.ts — Live-Queries fuer Chat, Rituals, Goals
tools.ts — Companion-eigene Tools (read_context, get_insights)
components/
CompanionChat.svelte — Chat-Interface mit Tool-Execution
CompanionFeed.svelte — Timeline von Nudges + Insights + Chat
RitualRunner.svelte — Step-by-Step Ritual-UI
GoalCard.svelte — Ziel-Fortschritts-Anzeige
11.2 Chat-Flow
User: "Wie laeuft mein Tag?"
|
v
CompanionChat → LLM Orchestrator
|
| System Prompt = Context Document (~500 Tokens)
| + Tool Schemas (getToolsForLlm())
| + User Message
|
v
LLM (Gemma lokal ODER Gemini Cloud)
|
| Response: "Du hast heute 3/7 Tasks erledigt und erst 400ml
| Wasser getrunken. Dein Kalender ist ab 15:00 frei — guter
| Zeitpunkt fuer die ueberfaelligen Tasks. Soll ich dich
| in 2 Stunden ans Wasser erinnern?"
|
| tool_use: create_reminder(...)
|
v
Tool Execution → drinkStore / remindersStore
|
v
CompanionChat zeigt Antwort + Aktions-Bestaetigung
11.3 Ritual-Generierung
User: "Erstell mir eine Morgenroutine"
|
v
LLM + Context Document + Tool Schemas
|
| LLM sieht: Nutzer hat Drink, Todo, Nutriphi, Calendar aktiv
| Memory: "Trinkt morgens zuerst Kaffee"
| Goals: "8 Glaeser Wasser/Tag"
|
v
Generiertes Ritual:
1. Glas Wasser loggen (tool: log_drink, water, 250ml)
2. Stimmung checken (free_text → journal)
3. Tages-Tasks priorisieren (zeigt DaySnapshot.tasks.dueToday)
4. Kalender-Ueberblick (zeigt DaySnapshot.events.upcoming)
5. Fruehstueck loggen (tool: log_meal)
12. Neue Dateien & Ordnerstruktur
apps/mana/apps/web/src/lib/
data/
events/
event-bus.ts — EventBus Singleton
event-store.ts — Persistenz in _events Tabelle
emit.ts — Helper fuer Module
types.ts — DomainEvent, EventMeta Interfaces
catalog.ts — Alle Event-Type Definitionen (union type)
projections/
day-snapshot.ts — DaySnapshot Aggregation
streaks.ts — Streak-Berechnung
correlations.ts — Statistische Korrelationen
context-document.ts — LLM-Prompt-Generator
tools/
types.ts — ModuleTool Interface
registry.ts — Tool-Sammlung + LLM-Schema-Generator
executor.ts — Tool-Ausfuehrung mit Validierung
companion/
rules/
types.ts — PulseRule, Nudge, RuleContext
engine.ts — Rule Runner (als ReminderSource)
water-reminder.ts
streak-warning.ts
morning-summary.ts
evening-reflection.ts
overdue-tasks.ts
meal-reminder.ts
goal-check.ts
feedback/
types.ts — NudgeOutcome
tracker.ts — Outcome-Recording
analyzer.ts — Pattern-Extraktion aus Outcomes
modules/
companion/
module.config.ts
collections.ts
stores/
chat.svelte.ts
rituals.svelte.ts
goals.svelte.ts
queries.ts
tools.ts
components/
CompanionChat.svelte
CompanionFeed.svelte
RitualRunner.svelte
GoalCard.svelte
todo/
tools.ts — NEU: Tool-Definitionen
stores/tasks.svelte.ts — ANPASSEN: emit() Calls
calendar/
tools.ts — NEU
stores/events.svelte.ts — ANPASSEN
drink/
tools.ts — NEU
stores/drink.svelte.ts — ANPASSEN
nutriphi/
tools.ts — NEU
mutations.ts — ANPASSEN
places/
tools.ts — NEU
stores/places.svelte.ts — ANPASSEN
stores/tracking.svelte.ts — ANPASSEN
13. Neue Dexie-Tabellen
// In database.ts, naechste Version:
// Event Store (ersetzt _activity langfristig)
_events: '++seq, type, [meta.appId+meta.timestamp], [meta.type+meta.timestamp], meta.recordId',
// Goals
goals: 'id, moduleId, status, [moduleId+status]',
goalHistory: '++id, goalId, periodStart',
// Semantic Memory
_memory: 'id, category, confidence, lastConfirmed, [category+confidence]',
// Feedback Loop
_nudgeOutcomes: '++id, nudgeId, nudgeType, outcome, timestamp, [nudgeType+outcome]',
// Companion Chat
companionConversations: 'id, createdAt',
companionMessages: 'id, conversationId, role, createdAt, [conversationId+createdAt]',
// Rituals
rituals: 'id, status, createdAt',
ritualSteps: 'id, ritualId, order, [ritualId+order]',
ritualLogs: '++id, ritualId, date, [ritualId+date]',
14. Implementierungs-Reihenfolge
Phase 1: Event-Fundament (Woche 1-2)
data/events/— EventBus, EventStore, emit Helper, Type Catalog- Domain Events fuer 5 Pilot-Module definieren (catalog.ts)
- Stores der 5 Module umbauen:
emit()Calls einfuegen - Event Store Subscriber:
eventBus.onAny()→_eventsTabelle - Tests: Events werden korrekt emittiert und persistiert
Ergebnis: Semantischer Event-Stream fliesst, Dexie-Writes + Events parallel.
Phase 2: Projections (Woche 2-3)
- DaySnapshot Projection (live Dexie-Queries + Event-Listener)
- Streaks Projection (basierend auf Events + TimeBlocks)
- Context Document Generator (Template-basiert)
- Dashboard-Widget: "Mein Tag" Karte mit DaySnapshot-Daten
Ergebnis: Zentraler Ueberblick ueber alle 5 Module, live-reaktiv.
Phase 3: Goals + Pulse (Woche 3-4)
- Goal Datenmodell + Store + Queries
- Goal-Tracking via Event-Subscription
- Goal-Templates (5 vordefinierte)
- Rule Engine mit 5 initialen Rules
- Integration in Reminder-Scheduler
- Nudge-UI: Toast / Bottom-Sheet
Ergebnis: Nutzer setzt Ziele, bekommt proaktive Nudges.
Phase 4: Tool Layer (Woche 4-5)
- ModuleTool Interface + Registry
- tools.ts fuer 5 Pilot-Module
- Tool Executor mit Validierung
- LLM Function Schema Generator
- Integration in LLM Orchestrator (
runWithTools)
Ergebnis: LLM kann Module lesen und beschreiben.
Phase 5: Companion Chat (Woche 5-6)
- Companion Modul (collections, stores, queries)
- CompanionChat Svelte-Komponente
- Chat-Flow: Context Document + Tools + LLM
- CompanionFeed: Timeline von Nudges + Chat
Ergebnis: Nutzer kann mit dem System sprechen und Aktionen ausfuehren.
Phase 6: Rituale (Woche 6-7)
- Ritual Datenmodell (steps, logs)
- RitualRunner Komponente
- AI-Ritual-Generierung via Companion Chat
- Vordefinierte Ritual-Templates (Morgen, Abend, Wasser)
Ergebnis: Gefuehrte Routinen die in Module schreiben.
Phase 7: Memory + Correlations (Woche 7-8)
- Semantic Memory Tabelle + Store
- Regelbasierte Pattern-Extraktion
- Correlation Engine ueber TimeBlocks
- Memory + Correlations in Context Document
- Feedback Loop (NudgeOutcome Tracking)
- LLM-basierte Memory-Extraktion (optional, Tier 1)
Ergebnis: System lernt ueber Zeit, Insights werden praeziser.
Phase 8: Rollout auf weitere Module (Woche 8+)
Pro Modul:
- Domain Events definieren (catalog.ts erweitern)
- Store Mutations mit emit() versehen
- tools.ts schreiben
- Projections erweitern (DaySnapshot Felder)
- Goal-Templates hinzufuegen
- Pulse Rules hinzufuegen
Geschaetzter Aufwand pro Modul: 1-2 Tage.
15. Abhaengigkeiten & Reihenfolge-Graph
Phase 1 (Events) ──────┬──> Phase 2 (Projections)
| |
| v
├──> Phase 3 (Goals + Pulse)
| |
v v
Phase 4 (Tools) ──> Phase 5 (Companion Chat)
|
v
Phase 6 (Rituale)
|
v
Phase 7 (Memory)
|
v
Phase 8 (Rollout)
Phase 1 ist Voraussetzung fuer alles. Phase 2-4 koennen teilweise parallel laufen.
16. Privacy-Garantien
| Daten | Verarbeitung | Speicherung |
|---|---|---|
| Domain Events | Lokal (Browser) | IndexedDB, encrypted |
| Projections | Lokal (Browser) | In-Memory, nicht persistiert |
| Goals | Lokal + Sync | IndexedDB → mana-sync (encrypted) |
| Memory Facts | Lokal (Browser) | IndexedDB, encrypted |
| Context Document | Lokal (Browser) | In-Memory, nie persistiert |
| LLM Inference | Tier 1: Browser (Gemma) | Kein Server-Kontakt |
| Tier 2: mana-llm (Ollama) | Context geht an eigenen Server | |
| Tier 3: Cloud (Gemini) | Nur mit explizitem Consent | |
| Nudge Outcomes | Lokal (Browser) | IndexedDB, nicht synced |
| Tool Execution | Lokal (Browser) | Writes gehen in Module-Tabellen |
Invariante: Sensitive Daten (Journal, Dreams, Finance, Nutriphi) werden nie an Tier 2/3 gesendet — erzwungen durch contentClass: 'sensitive' im LLM Orchestrator.
17. Migration: _activity → _events
Die _activity-Tabelle bleibt vorerst bestehen (Sync-Debugging). Langfristig:
- Phase 1-2:
_eventswird parallel zu_activitybefuellt - Phase 7: Alle Consumers (Activity-Page, Debug-Tools) auf
_eventsumstellen - Danach:
_activity-Befuellung aus Hooks entfernen, Tabelle als deprecated markieren - Naechste Major-Version: Tabelle loeschen
18. Testing-Strategie
Unit Tests
- Event Bus: emit/subscribe/unsubscribe, ordering, no re-entrant loops
- Projections: DaySnapshot korrekt aus Mock-Daten, Streak-Berechnung
- Rules: Nudge-Generierung unter verschiedenen Bedingungen
- Tools: Parameter-Validierung, Execute-Flow
- Correlations: Pearson-Berechnung, Signifikanz-Filter
Integration Tests
- Store emit() → EventBus → EventStore → Projection Update
- Rule Engine → Nudge → UI → Outcome → Memory Update
- Companion Chat → LLM Mock → Tool Call → Store Mutation → Event
E2E Tests
- Morgenroutine-Ritual durchspielen: 5 Steps → Daten in 3 Modulen
- Wasser-Ziel erreichen: 8x log_drink → GoalReached Event → Nudge
- Companion-Frage: "Wie war meine Woche?" → Context Document → Antwort