mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
- App display name → Cardecky in mana-apps.ts, MODULE_REGISTRY, alle Docs - Domains: cardecky.mana.how (App), cardecky-api.mana.how (Marketplace API), cardecky.com (Marketing-Landing — cloudflared-route + nginx-Block vorbereitet, DNS muss noch gesetzt werden) - 301-Redirect cards.mana.how → cardecky.mana.how (nginx + cloudflared) für alte Bookmarks; kann nach 6–12 Monaten wieder raus - SPDX license IDs Cards-Personal-Use/Pro-Only-1.0 → Cardecky-* via Drizzle 0001-Migration (DROP CHECK → UPDATE rows → SET DEFAULT → ADD CHECK), inkl. _journal- und 0001_snapshot-Update - In-mana cards-Modul: dezenter Banner zur Standalone-App (GUIDELINES §12), einmal schließbar via localStorage - Docker-CORS-Listen, sso-origins.ts, Prometheus-Target aktualisiert Technische IDs bleiben bewusst: appId 'cards', schema mana_platform.cards.*, Verzeichnis apps/cards/, Package @cards/web, services/cards-server, Env-Vars CARDS_*, UMAMI_WEBSITE_ID_CARDS*, Class CardsEvents — Mana-Konvention (Brand ≠ technischer Identifier). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2039 lines
81 KiB
Markdown
2039 lines
81 KiB
Markdown
# 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, Food, 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:**
|
||
1. **Pulse** — Regelbasierte Nudges & Tageszusammenfassungen (kein LLM)
|
||
2. **Rituale** — Gefuehrte Routinen die Daten in Module schreiben (AI-generiert)
|
||
3. **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 - Food - 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`**
|
||
|
||
```typescript
|
||
// ── 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: `_emitting` Set verhindert re-entrant emits vom selben Event-Typ
|
||
|
||
```typescript
|
||
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 |
|
||
|
||
#### Food 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:**
|
||
|
||
```typescript
|
||
// 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`**
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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?"
|
||
|
||
```typescript
|
||
// 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 };
|
||
};
|
||
|
||
// Food
|
||
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?"
|
||
|
||
```typescript
|
||
// 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?"
|
||
|
||
```typescript
|
||
// 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)
|
||
- Food: 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]'
|
||
```
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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 (Food, event_sum, MealLogged, calories)
|
||
- 5 Tasks/Tag erledigen (Todo, event_count, TaskCompleted)
|
||
- Alle Mahlzeiten tracken (Food, 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]'
|
||
```
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
1. **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"
|
||
|
||
2. **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
|
||
|
||
```typescript
|
||
// 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`**
|
||
|
||
```typescript
|
||
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`:**
|
||
|
||
```typescript
|
||
// 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/food/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`**
|
||
|
||
```typescript
|
||
import { todoTools } from '$lib/modules/todo/tools';
|
||
import { calendarTools } from '$lib/modules/calendar/tools';
|
||
import { drinkTools } from '$lib/modules/drink/tools';
|
||
import { foodTools } from '$lib/modules/food/tools';
|
||
import { placesTools } from '$lib/modules/places/tools';
|
||
|
||
const ALL_TOOLS: ModuleTool[] = [
|
||
...todoTools,
|
||
...calendarTools,
|
||
...drinkTools,
|
||
...foodTools,
|
||
...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:
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
1. **Event-getriggert:** Reagiert auf Domain Events (z.B. `TaskCompleted` → Streak-Check)
|
||
2. **Zeitgesteuert:** Periodische Checks (Morgen-Summary, Abend-Reflexion, stuendlich)
|
||
|
||
### 9.2 Rule Interface
|
||
|
||
**Neues File: `apps/mana/apps/web/src/lib/companion/rules/types.ts`**
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
// 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]'
|
||
```
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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, Food, 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. Dateien & Ordnerstruktur
|
||
|
||
✅ = implementiert, ⬜ = ausstehend
|
||
|
||
```
|
||
apps/mana/apps/web/src/lib/
|
||
data/
|
||
events/ ✅ Phase 1
|
||
event-bus.ts ✅ EventBus Singleton (sync dispatch, microtask handlers)
|
||
event-store.ts ✅ Persistenz in _events Tabelle (90d TTL, 50k max)
|
||
emit.ts ✅ emitDomainEvent() Helper
|
||
types.ts ✅ DomainEvent, EventMeta, EventBus Interfaces
|
||
catalog.ts ✅ 22 Event-Typen (ManaEvent union type)
|
||
index.ts ✅ Barrel Export
|
||
projections/ ✅ Phase 2
|
||
day-snapshot.ts ✅ useDaySnapshot() — live Tagesaggregation
|
||
streaks.ts ✅ useStreaks() — 3 Streak-Typen, 90d Lookback
|
||
context-document.ts ✅ generateContextDocument() — ~500 Token LLM-Prompt
|
||
correlations.ts ✅ Phase 7 — Pearson ueber 7 Metriken
|
||
types.ts ✅ DaySnapshot, StreakInfo, TaskSummary, EventSummary
|
||
index.ts ✅ Barrel Export
|
||
tools/ ✅ Phase 4
|
||
types.ts ✅ ModuleTool, ToolParameter, ToolResult, LlmFunctionSchema
|
||
registry.ts ✅ registerTools(), getToolsForLlm()
|
||
executor.ts ✅ executeTool() mit Validierung + Typ-Coercion
|
||
init.ts ✅ initTools() — registriert alle 5 Module
|
||
index.ts ✅ Barrel Export
|
||
companion/
|
||
goals/ ✅ Phase 3
|
||
types.ts ✅ LocalGoal, GoalMetric, GoalTarget, 6 Templates
|
||
store.ts ✅ CRUD + Event-Bus-Subscription fuer Progress
|
||
queries.ts ✅ useActiveGoals(), useAllGoals()
|
||
index.ts ✅ Barrel Export
|
||
rules/ ✅ Phase 3
|
||
types.ts ✅ PulseRule, Nudge, NudgeType, RuleContext
|
||
rules.ts ✅ 5 Rules (water, streak, morning, overdue, meal)
|
||
engine.ts ✅ evaluateRules(), createPulseReminderSource()
|
||
index.ts ✅ Barrel Export
|
||
feedback/ ✅ Phase 3
|
||
types.ts ✅ NudgeOutcome
|
||
tracker.ts ✅ recordOutcome(), getOutcomeStats(), getActionRate()
|
||
index.ts ✅ Barrel Export
|
||
memory/ ✅ Phase 7
|
||
types.ts ✅ MemoryFact, Correlation
|
||
store.ts ✅ recordFact, contradictFact, applyDecay, getFacts
|
||
extractors.ts ✅ 3 Extractors (day-of-week, time-of-day, frequency)
|
||
index.ts ✅ Barrel Export
|
||
rituals/ ✅ Phase 6
|
||
types.ts ✅ 6 Step-Typen, 3 Templates
|
||
store.ts ✅ createFromTemplate, CRUD, logs
|
||
queries.ts ✅ useActiveRituals, useAllRituals
|
||
index.ts ✅ Barrel Export
|
||
modules/
|
||
companion/ ✅ Phase 5
|
||
types.ts ✅ LocalConversation, LocalMessage
|
||
collections.ts ✅ companionConversations, companionMessages
|
||
stores/chat.svelte.ts ✅ Conversation + Message CRUD
|
||
queries.ts ✅ useConversations, useMessages
|
||
engine.ts ✅ runCompanionChat (LLM + Tools + Context)
|
||
index.ts ✅ Barrel Export
|
||
components/
|
||
CompanionChat.svelte ✅ Chat-UI mit Streaming + Tool-Results
|
||
RitualRunner.svelte ✅ Step-by-Step Runner
|
||
CompanionFeed.svelte ⬜ Timeline (spaetere Iteration)
|
||
GoalCard.svelte ⬜ Goal-Fortschritts-Widget (spaetere Iteration)
|
||
todo/
|
||
tools.ts ✅ 3 Tools (create, complete, stats)
|
||
stores/tasks.svelte.ts ✅ 5 Events (Created, Completed, Uncompleted, Deleted, Subtasks)
|
||
calendar/
|
||
tools.ts ✅ 2 Tools (create_event, get_todays_events)
|
||
stores/events.svelte.ts ✅ 3 Events (Created, Updated, Deleted)
|
||
drink/
|
||
tools.ts ✅ 3 Tools (log, progress, undo)
|
||
stores/drink.svelte.ts ✅ 3 Events (Logged, Deleted, Undone)
|
||
food/
|
||
tools.ts ✅ 2 Tools (log_meal, nutrition_summary)
|
||
mutations.ts ✅ 3 Events (Logged, PhotoLogged, Deleted)
|
||
places/
|
||
tools.ts ✅ 4 Tools (create, visit, get_places, location)
|
||
stores/places.svelte.ts ✅ 3 Events (Created, Deleted, Visited)
|
||
stores/tracking.svelte.ts ✅ 3 Events (Started, Stopped, LocationLogged)
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Dexie-Tabellen
|
||
|
||
### Implementiert (v10 Schema)
|
||
|
||
```typescript
|
||
// Event Store — append-only domain event log
|
||
_events: '++seq, type, meta.appId, meta.timestamp, meta.recordId, [meta.appId+meta.timestamp], [type+meta.timestamp]',
|
||
|
||
// Goals — companion brain goal tracking
|
||
companionGoals: 'id, moduleId, status, [moduleId+status]',
|
||
|
||
// Semantic Memory — extracted user patterns (prepared, not yet populated)
|
||
_memory: 'id, category, confidence, lastConfirmed, [category+confidence]',
|
||
|
||
// Feedback Loop — nudge outcome tracking
|
||
_nudgeOutcomes: '++id, nudgeId, nudgeType, outcome, timestamp, [nudgeType+outcome]',
|
||
```
|
||
|
||
### Noch ausstehend (Phase 5+)
|
||
|
||
```typescript
|
||
// Companion Chat (Phase 5)
|
||
companionConversations: 'id, createdAt',
|
||
companionMessages: 'id, conversationId, role, createdAt, [conversationId+createdAt]',
|
||
|
||
// Rituals (Phase 6)
|
||
rituals: 'id, status, createdAt',
|
||
ritualSteps: 'id, ritualId, order, [ritualId+order]',
|
||
ritualLogs: '++id, ritualId, date, [ritualId+date]',
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Implementierungs-Reihenfolge
|
||
|
||
### Phase 1: Event-Fundament — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `e927c1f10`
|
||
|
||
1. ✅ `data/events/` — EventBus, EventStore, emit Helper, Type Catalog
|
||
2. ✅ Domain Events fuer 5 Pilot-Module definiert (catalog.ts, 22 Event-Typen)
|
||
3. ✅ Stores der 5 Module umgebaut: `emit()` Calls eingefuegt
|
||
4. ✅ Event Store Subscriber: `eventBus.onAny()` → `_events` Tabelle (v10 Schema)
|
||
5. ⬜ Tests: noch ausstehend
|
||
|
||
**Ergebnis:** Semantischer Event-Stream fliesst. 20 Domain Events aus 5 Modulen.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Events werden im Store emittiert (nicht im Dexie-Hook) — der Store kennt die Semantik
|
||
- `emitDomainEvent()` Helper reduziert Boilerplate auf eine Zeile pro Event
|
||
- Re-Entrancy-Guard im EventBus verhindert Endlos-Loops
|
||
- `_activity` Tabelle bleibt parallel bestehen (Sync-Debugging)
|
||
|
||
### Phase 2: Projections — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `40e1145e9`
|
||
|
||
1. ✅ DaySnapshot Projection (live Dexie-Queries ueber alle 5 Module)
|
||
2. ✅ Streaks Projection (3 Streak-Definitionen: Wasser, Tasks, Mahlzeiten, 90-Tage Lookback)
|
||
3. ✅ Context Document Generator (Template-basiert, ~300-500 Token)
|
||
4. ⬜ Dashboard-Widget: "Mein Tag" Karte — spaeter in UI-Phase
|
||
|
||
**Ergebnis:** Zentraler Ueberblick ueber alle 5 Module, live-reaktiv.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Projections nutzen `useLiveQueryWithDefault` aus `@mana/local-store/svelte`
|
||
- DaySnapshot queried 5 Dexie-Tabellen + decrypted in einem buildSnapshot()-Call
|
||
- Streaks berechnen per checkDate() ob ein Tag "zaehlt" (z.B. Wasser-Ziel erreicht)
|
||
- Context Document ist reines String-Template, kein LLM noetig
|
||
- `startEventStore()` in `(app)/+layout.svelte` bei Auth-Ready gewired
|
||
|
||
### Phase 3: Goals + Pulse — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `9066b6c9a`
|
||
|
||
1. ✅ Goal Datenmodell + Store + Queries (`companion/goals/`)
|
||
2. ✅ Goal-Tracking via Event-Bus-Subscription (auto-increment currentValue)
|
||
3. ✅ 6 Goal-Templates (Wasser, Tasks, Mahlzeiten, Kalorien, Orte, Kaffee-Limit)
|
||
4. ✅ Rule Engine mit 5 Rules (`companion/rules/`)
|
||
5. ✅ ReminderSource-Adapter fuer bestehenden Scheduler
|
||
6. ⬜ Nudge-UI: Toast / Bottom-Sheet — in Phase 5 (Companion Chat)
|
||
|
||
**Ergebnis:** Goals tracken automatisch, Rules erzeugen Nudges.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Goals leben in `companionGoals` Tabelle (v10 Schema), nicht im Core-Modul
|
||
- Goal-Tracker subscribed auf `eventBus.onAny()` und matched per eventType + Filter
|
||
- Perioden-Reset (day/week/month) passiert automatisch beim naechsten Event
|
||
- `GoalReached` Event wird emittiert wenn Ziel erstmals in einer Periode erreicht
|
||
- Rules nutzen localStorage fuer Dismissal-Tracking und Last-Run-Timestamps
|
||
- `_memory` und `_nudgeOutcomes` Tabellen vorbereitet (v10 Schema)
|
||
|
||
### Phase 4: Tool Layer — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `66dd684bb`
|
||
|
||
1. ✅ ModuleTool Interface + Registry (dynamische Registrierung)
|
||
2. ✅ tools.ts fuer 5 Pilot-Module (13 Tools total)
|
||
3. ✅ Tool Executor mit Parameter-Validierung + Typ-Coercion
|
||
4. ✅ LLM Function Schema Generator (`getToolsForLlm()`)
|
||
5. ⬜ Integration in LLM Orchestrator (`runWithTools`) — in Phase 5
|
||
|
||
**Ergebnis:** 13 Tools bereit fuer LLM Function-Calling.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Registry nutzt `registerTools()` Pattern statt statische Imports (tree-shaking-freundlich)
|
||
- `initTools()` in `(app)/+layout.svelte` gewired neben `startEventStore()`
|
||
- Executor coerced String→Number und String→Boolean automatisch
|
||
- Tools pro Modul: Todo (3), Calendar (2), Drink (3), Food (2), Places (4)
|
||
- Jeder Tool hat eine `message` Feld fuer menschenlesbare Bestaetigung
|
||
|
||
### Phase 5: Companion Chat — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `46db527f8`
|
||
|
||
1. ✅ Companion Modul (types, collections, stores/chat, queries)
|
||
2. ✅ CompanionChat Svelte-Komponente (Streaming, Tool-Results inline)
|
||
3. ✅ Chat-Flow: Context Document als System-Prompt + Tool Schemas + LLM
|
||
4. ✅ `/companion` Route mit Sidebar (Gespraechsliste) + Chat-Area
|
||
5. ⬜ CompanionFeed: Timeline von Nudges + Chat — spaetere UI-Iteration
|
||
|
||
**Ergebnis:** Nutzer kann mit dem System sprechen und Aktionen ausfuehren.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Chat nutzt `@mana/local-llm` (Gemma, browser-lokal) direkt — kein Server-Call
|
||
- Tool Calling via JSON-Block-Extraction (`\`\`\`tool {...}\`\`\``) statt nativem Function-Calling (Gemma unterstuetzt das nicht nativ)
|
||
- Max 3 Tool-Call-Runden pro Nachricht (verhindert Endlos-Loops)
|
||
- Conversations + Messages persistent in IndexedDB (`companionConversations`, `companionMessages`)
|
||
- Entscheidung: Companion lebt als eigenes Modul unter `/companion`, nicht als Overlay
|
||
- Streaming via `onToken` Callback — erster Round streamt, Folgerunden (nach Tool-Call) nicht
|
||
|
||
### Phase 6: Rituale — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `41357b254`
|
||
|
||
1. ✅ Ritual Datenmodell (6 Step-Typen: tool_call, number_input, text_input, mood_picker, info_display, checklist)
|
||
2. ✅ RitualRunner Komponente (Step-Card-UI, Progress-Bar, Tool-Execution)
|
||
3. ⬜ AI-Ritual-Generierung via Companion Chat — spaetere Iteration
|
||
4. ✅ 3 Ritual-Templates (Morgenroutine, Abendroutine, Trink-Check)
|
||
5. ✅ `/companion/rituals` Route mit Template-Picker + Ritual-Liste
|
||
|
||
**Ergebnis:** Gefuehrte Routinen die in Module schreiben.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Rituale leben in `companion/rituals/` (nicht als eigenes Modul)
|
||
- Steps referenzieren Tools per Name — dieselben Tools die der Chat nutzt
|
||
- `info_display` Steps zeigen Projektionsdaten (Tasks, Events, Drinks, Nutrition, Streaks)
|
||
- Completion-Logs tracken wieviele Steps pro Tag abgeschlossen wurden
|
||
- Templates sind statisch definiert — AI-Generierung wird in Phase 5 integriert
|
||
|
||
### Phase 7: Memory + Correlations — ERLEDIGT (2026-04-13)
|
||
|
||
Commit: `87a1dd682`
|
||
|
||
1. ✅ Semantic Memory Store (`_memory` Tabelle, Confidence-Lifecycle)
|
||
2. ✅ 3 regelbasierte Pattern-Extractors (11 Extraction-Rules ueber 5 Module)
|
||
3. ✅ Correlation Engine (Pearson ueber 7 Metriken, cross-modul)
|
||
4. ✅ Memory + Correlations in Context Document integriert
|
||
5. ✅ Feedback Loop: `_nudgeOutcomes` Tabelle + Tracker (Phase 3)
|
||
6. ⬜ LLM-basierte Memory-Extraktion — spaetere Iteration
|
||
|
||
**Ergebnis:** System lernt Muster, findet Korrelationen, alles fliesst ins LLM.
|
||
|
||
**Implementierungsnotizen:**
|
||
- Pattern Extractors: day-of-week (Wochentags-Muster), time-of-day (4h-Peak-Fenster), frequency (Tages-Durchschnitt)
|
||
- Confidence: 0.3 initial, +0.15 pro Bestaetigung, -0.15 bei Widerspruch, Decay nach 30 Tagen
|
||
- Correlations nur cross-modul (gleich-Modul wird uebersprungen, trivial korreliert)
|
||
- Nur Korrelationen mit |r| >= 0.3 und >= 14 Tage Daten werden behalten
|
||
- `extractAllPatterns()` soll taeglich laufen (manuell oder via Scheduled Rule)
|
||
- `computeCorrelations()` berechnet on-demand, nicht persistent gecached
|
||
|
||
### Phase 8: Rollout auf weitere Module (Woche 8+)
|
||
|
||
Pro Modul:
|
||
1. Domain Events definieren (catalog.ts erweitern)
|
||
2. Store Mutations mit emit() versehen
|
||
3. tools.ts schreiben
|
||
4. Projections erweitern (DaySnapshot Felder)
|
||
5. Goal-Templates hinzufuegen
|
||
6. 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)
|
||
```
|
||
|
||
**Status: Phase 1-8 ERLEDIGT (2026-04-13).** 29 von ~40 Modulen angebunden.
|
||
|
||
---
|
||
|
||
## 15b. Phase 8 Status: Modul-Rollout
|
||
|
||
### Angebundene Module (29)
|
||
|
||
| # | Modul | Events | Tools | Batch |
|
||
|---|-------|--------|-------|-------|
|
||
| 1 | Todo | 5 | 3 | Pilot |
|
||
| 2 | Calendar | 3 | 2 | Pilot |
|
||
| 3 | Drink | 3 | 3 | Pilot |
|
||
| 4 | Food | 3 | 2 | Pilot |
|
||
| 5 | Places | 6 | 4 | Pilot |
|
||
| 6 | Habits | 3 | 3 | Batch 2 |
|
||
| 7 | Journal | 3 | 2 | Batch 2 |
|
||
| 8 | Notes | 2 | 1 | Batch 2 |
|
||
| 9 | Contacts | 2 | 2 | Batch 2 |
|
||
| 10 | Body | 5 | 3 | Batch 2 |
|
||
| 11 | Finance | 2 | 1 | Batch 3 |
|
||
| 12 | Dreams | 2 | 1 | Batch 3 |
|
||
| 13 | Cardecky | 2 | 1 | Batch 3 |
|
||
| 14 | Times | 2 | 2 | Batch 3 |
|
||
| 15 | Social Events | 2 | 1 | Batch 3 |
|
||
| 16 | Music | 1 | 1 | Batch 4 |
|
||
| 17 | Storage | 1 | 1 | Batch 4 |
|
||
| 18 | Chat | 2 | 1 | Batch 4 |
|
||
| 19 | Memoro | 1 | 1 | Batch 4 |
|
||
| 20 | Skilltree | 2 | 2 | Batch 4 |
|
||
| 21 | Period | 1 | 1 | Batch 5 |
|
||
| 22 | Firsts | 1 | 1 | Batch 5 |
|
||
| 23 | Guides | 1 | 1 | Batch 5 |
|
||
| 24 | Inventory | 1 | 1 | Batch 5 |
|
||
| 25 | Photos | 1 | 0 | Batch 5 |
|
||
| 26 | Plants | 2 | 1 | Batch 6 |
|
||
| 27 | News | 1 | 0 | Batch 6 |
|
||
| 28 | Recipes | 2 | 1 | Batch 6 |
|
||
| 29 | Questions | 1 | 0 | Batch 6 |
|
||
| **Total** | | **67** | **47** | |
|
||
|
||
### Noch fehlende Module (~11)
|
||
|
||
| Modul | Grund | Prioritaet |
|
||
|-------|-------|-----------|
|
||
| Citycorners | Nischen-Modul (Konstanz-Guide) | Niedrig |
|
||
| Uload | URL-Shortener, wenig Brain-relevant | Niedrig |
|
||
| Calc | Kein persistenter State | Nicht noetig |
|
||
| Moodlit | Ambient-Lighting, kein Tracking | Nicht noetig |
|
||
| Playground | Dev-Tool fuer LLM-Tests | Nicht noetig |
|
||
| Who | Rate-Spiel, kein Tracking | Nicht noetig |
|
||
| Quotes | Zitate (read-only) | Nicht noetig |
|
||
| Context | Kein eigener Store / Mutations | Nicht noetig |
|
||
| Presi | Praesentation-Builder | Niedrig |
|
||
| Meditate | Meditation-Sessions | Mittel |
|
||
| Sleep | Schlaf-Tracking | Mittel |
|
||
|
||
**Empfehlung:** Meditate und Sleep lohnen sich fuer Correlations (Schlaf vs. Produktivitaet). Die anderen sind entweder read-only, Dev-Tools oder haben keinen persistenten State der fuer das Brain relevant waere.
|
||
|
||
---
|
||
|
||
## 15c. Bekannte Altlasten & Optimierungs-Potenzial
|
||
|
||
### Altlast: `_activity` Tabelle
|
||
|
||
Die alte `_activity`-Tabelle wird weiterhin parallel zum neuen `_events` Event Store befuellt (via Dexie-Hooks in database.ts). Sie enthaelt nur CRUD-Operationen ohne Semantik. **Kann entfernt werden** sobald alle Debug-Tools und die Activity-Seite auf `_events` umgestellt sind.
|
||
|
||
**TODO:** `trackActivity()` Calls in database.ts:546-638 entfernen und Activity-Query in activity.ts auf `queryEvents()` umstellen.
|
||
|
||
### Altlast: Trigger-System duplikation
|
||
|
||
Das bestehende Trigger-System (`lib/triggers/`) feuert ebenfalls bei Dexie-Writes und hat eigene Actions (logHabit, createTask, createNote). Das Companion Brain hat ein eigenes, maechtigeres System (Domain Events + Goals + Rules). Langfristig sollte das alte Trigger-System in die Rule Engine migriert werden.
|
||
|
||
**TODO:** Bestehende Automations (`automations` Tabelle) als Pulse Rules abbilden, altes Trigger-System entfernen.
|
||
|
||
### Optimierung: Streaks-Berechnung
|
||
|
||
`useStreaks()` in streaks.ts berechnet fuer jeden Streak bis zu 90 Tage zurueck — pro Streak eine separate Dexie-Query pro Tag (worst case: 3 Streaks x 90 Tage = 270 Queries). Fuer die Pilotphase akzeptabel, langfristig sollte das via Event-basierte inkrementelle Berechnung ersetzt werden (Event "DrinkGoalReached" → Streak +1 statt taeglich zurueckschauen).
|
||
|
||
### Optimierung: DaySnapshot Query-Last
|
||
|
||
`buildSnapshot()` in day-snapshot.ts queried 5+ Dexie-Tabellen sequentiell + decrypted jeweils. Bei grossen Datenmengen koennte das >100ms dauern. Moegliche Optimierungen:
|
||
- Parallele Queries via `Promise.all()`
|
||
- Caching des Snapshots fuer 30s (statt bei jedem liveQuery-Trigger neu berechnen)
|
||
- Event-basiertes inkrementelles Update statt Full-Scan
|
||
|
||
### Optimierung: Context Document fuer LLM
|
||
|
||
Der Context Document Generator ist aktuell ein reines String-Template. Wenn das LLM-Modell besser wird (groesseres Kontextfenster), koennte das Dokument um historische Daten erweitert werden (letzte Woche, Trends). Aktuell auf ~500 Tokens optimiert fuer Gemma 4 E2B (2B Modell).
|
||
|
||
### Optimierung: Companion Chat ohne WebGPU
|
||
|
||
Der Chat funktioniert aktuell NUR mit WebGPU (Gemma lokal). Fuer Browser ohne WebGPU (Firefox, Safari) gibt es keinen Fallback. **TODO:** Server-Fallback via `mana-llm` Ollama-Endpoint integrieren, gesteuert ueber den bestehenden LLM Orchestrator Tier-System.
|
||
|
||
### Feature-Luecke: Goal-UI
|
||
|
||
Goals haben kein eigenes UI ausser der Workbench "Ziele" Page. Es gibt keine Moeglichkeit fuer den Nutzer, eigene Goals frei zu definieren (nur Templates). **TODO:** Goal-Editor-Modal mit Metric/Target-Builder.
|
||
|
||
### Feature-Luecke: Pulse Nudge-UI
|
||
|
||
Pulse Rules erzeugen Nudges, aber diese werden nur als OS-Notifications angezeigt (via Reminder-Scheduler). Es gibt keine In-App-Anzeige. **TODO:** NudgeToast Komponente oder Integration in den CompanionFeed.
|
||
|
||
---
|
||
|
||
## 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, Food) 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:
|
||
|
||
1. Phase 1-2: `_events` wird parallel zu `_activity` befuellt
|
||
2. Phase 7: Alle Consumers (Activity-Page, Debug-Tools) auf `_events` umstellen
|
||
3. Danach: `_activity`-Befuellung aus Hooks entfernen, Tabelle als deprecated markieren
|
||
4. 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
|
||
|
||
---
|
||
|
||
## 19. Manuelles Testen (Anleitung)
|
||
|
||
### Voraussetzungen
|
||
|
||
```bash
|
||
pnpm run mana:dev # Startet das Dev-Server auf :5173
|
||
```
|
||
|
||
Oeffne http://localhost:5173 im Browser (Chrome/Edge mit WebGPU-Support fuer LLM).
|
||
|
||
### 1. Event Bus verifizieren
|
||
|
||
Oeffne die Browser DevTools Console und teste:
|
||
|
||
```javascript
|
||
// Zugriff auf den Event Bus (global via Module-Import im App-Kontext)
|
||
// Am einfachsten: Daten erzeugen und in IndexedDB pruefen
|
||
|
||
// 1. Erstelle einen Task in der Todo-App
|
||
// 2. Oeffne DevTools → Application → IndexedDB → mana → _events
|
||
// 3. Dort sollte ein Event mit type="TaskCreated" erscheinen
|
||
|
||
// Alternativ via Console:
|
||
const db = await indexedDB.open('mana');
|
||
// Events sind in der _events Tabelle mit type, payload, meta
|
||
```
|
||
|
||
**Was zu pruefen ist:**
|
||
- Neuen Task erstellen → `TaskCreated` Event in `_events`
|
||
- Task erledigen → `TaskCompleted` Event
|
||
- Drink loggen → `DrinkLogged` Event
|
||
- Mahlzeit loggen → `MealLogged` Event
|
||
- Ort besuchen → `PlaceVisited` Event
|
||
|
||
### 2. Projections testen
|
||
|
||
Die Projections sind live-reaktiv. Am einfachsten via Browser Console:
|
||
|
||
```javascript
|
||
// DaySnapshot ist eine Svelte-reaktive Query.
|
||
// In einer Svelte-Komponente:
|
||
// import { useDaySnapshot } from '$lib/data/projections';
|
||
// const day = useDaySnapshot();
|
||
// console.log(day.value);
|
||
|
||
// Zum manuellen Testen: Daten erzeugen und schauen ob DaySnapshot reagiert
|
||
// 1. Gehe zu /drink und logge ein Glas Wasser
|
||
// 2. Die DaySnapshot.drinks.water.ml sollte sich erhoehen
|
||
// 3. Gehe zu /todo und erstelle+erledige einen Task
|
||
// 4. DaySnapshot.tasks.completed sollte steigen
|
||
```
|
||
|
||
### 3. Companion Chat testen
|
||
|
||
1. Navigiere zu `/companion`
|
||
2. Klicke "Gespraech starten"
|
||
3. **WICHTIG:** Der Chat nutzt `@mana/local-llm` (Gemma, ~500MB Download).
|
||
Beim ersten Mal muss das Modell heruntergeladen werden. Das dauert
|
||
je nach Verbindung 1-5 Minuten. WebGPU muss verfuegbar sein
|
||
(Chrome 113+, Edge 113+, kein Firefox/Safari).
|
||
4. Teste Nachrichten:
|
||
- "Wie sieht mein Tag aus?" → Sollte DaySnapshot-Daten zusammenfassen
|
||
- "Log mir ein Glas Wasser" → Sollte `log_drink` Tool aufrufen
|
||
- "Erstelle einen Task: Einkaufen gehen" → Sollte `create_task` Tool aufrufen
|
||
- "Wie viel Wasser habe ich heute getrunken?" → Nutzt Context Document
|
||
|
||
**Ohne WebGPU (Fallback):** Der Chat wird fehlschlagen wenn kein WebGPU
|
||
verfuegbar ist. In dem Fall die Engine temporaer auf einen Server-Endpoint
|
||
umbauen oder den Chat-Flow mit Mock-Responses testen.
|
||
|
||
### 4. Rituale testen
|
||
|
||
1. Navigiere zu `/companion/rituals`
|
||
2. Klicke "+ Neu" → waehle "Morgenroutine"
|
||
3. Klicke den Play-Button neben der erstellten Routine
|
||
4. Der RitualRunner zeigt Step fuer Step:
|
||
- Step 1: "Glas Wasser trinken" → Klick "Ausfuehren" → loggt 250ml Wasser
|
||
- Step 2: "Dein Tag auf einen Blick" → zeigt heutige Events
|
||
- Step 3: "Heutige Tasks" → zeigt faellige Tasks
|
||
- Step 4: "Deine Streaks" → zeigt Streak-Status
|
||
5. "Weiter" / "Fertig" navigiert durch die Steps
|
||
6. Pruefe in `/drink` ob das Wasser tatsaechlich geloggt wurde
|
||
|
||
### 5. Goals testen
|
||
|
||
Goals sind aktuell nur programmatisch testbar (kein UI). Via Console:
|
||
|
||
```javascript
|
||
// In einer Svelte-Komponente oder via Hot-Module:
|
||
import { goalStore, GOAL_TEMPLATES } from '$lib/companion/goals';
|
||
|
||
// Goal aus Template erstellen
|
||
const goal = await goalStore.createFromTemplate(GOAL_TEMPLATES[0]); // "8 Glaeser Wasser"
|
||
console.log(goal);
|
||
|
||
// Jetzt ein Wasser loggen → Goal currentValue sollte steigen
|
||
// (der Goal-Tracker subscribed auf den Event Bus)
|
||
```
|
||
|
||
### 6. Memory + Correlations testen
|
||
|
||
Braucht mindestens 7-14 Tage an Daten. Zum Testen mit Seed-Daten:
|
||
|
||
```javascript
|
||
// Pattern Extraction manuell ausfuehren:
|
||
import { extractAllPatterns } from '$lib/companion/memory';
|
||
await extractAllPatterns();
|
||
|
||
// Extrahierte Facts pruefen:
|
||
import { memoryStore } from '$lib/companion/memory';
|
||
const facts = await memoryStore.getFacts();
|
||
console.log(facts);
|
||
|
||
// Correlations berechnen:
|
||
import { computeCorrelations } from '$lib/data/projections';
|
||
const corrs = await computeCorrelations();
|
||
console.log(corrs);
|
||
```
|
||
|
||
**Hinweis:** Mit weniger als 7 Events pro Typ oder 14 Tagen Daten liefern
|
||
die Extractors und Correlations keine Ergebnisse — das ist beabsichtigt,
|
||
um Rauschen zu vermeiden.
|
||
|
||
### 7. Pulse Rules testen
|
||
|
||
Rules laufen ueber den Reminder-Scheduler (30s Intervall). Zum manuellen Test:
|
||
|
||
```javascript
|
||
import { evaluateRules } from '$lib/companion/rules';
|
||
import { useDaySnapshot } from '$lib/data/projections/day-snapshot';
|
||
import { useStreaks } from '$lib/data/projections/streaks';
|
||
|
||
// In einer Komponente:
|
||
const day = useDaySnapshot();
|
||
const streaks = useStreaks();
|
||
const nudges = evaluateRules(day.value, streaks.value, []);
|
||
console.log(nudges);
|
||
```
|
||
|
||
### 8. IndexedDB direkt inspizieren
|
||
|
||
Alle Companion-Daten liegen in IndexedDB (`mana` Database):
|
||
|
||
| Tabelle | Inhalt |
|
||
|---------|--------|
|
||
| `_events` | Domain Event Stream (type, payload, meta) |
|
||
| `companionGoals` | Aktive Ziele mit currentValue |
|
||
| `companionConversations` | Chat-Gespraeche |
|
||
| `companionMessages` | Chat-Nachrichten + Tool-Calls |
|
||
| `rituals` | Erstellte Rituale |
|
||
| `ritualSteps` | Ritual-Steps (pro Ritual) |
|
||
| `ritualLogs` | Completion-Logs |
|
||
| `_memory` | Extrahierte Muster (nach extractAllPatterns) |
|
||
| `_nudgeOutcomes` | Nudge-Reaktionen |
|
||
| `pendingProposals` | Staged AI-Intents (siehe §20) |
|
||
|
||
Oeffne: DevTools → Application → IndexedDB → mana → [Tabelle]
|
||
|
||
---
|
||
|
||
## 20. AI Workbench (ab 2026-04-14)
|
||
|
||
Der Companion wird schrittweise vom **Chatbot-mit-Tools** zum **zweiten Akteur im System**:
|
||
er arbeitet parallel zum Menschen in den bestehenden Modulen, User sieht jede Aenderung
|
||
inline und approved / reverted wo noetig. Fundament laeuft; Missions + Runner folgen.
|
||
|
||
### 20.1 Actor-Modell
|
||
|
||
Jeder Write im System traegt ab jetzt einen expliziten Actor. Source of Truth ist die
|
||
Data-Schicht (Events + Records + Sync-Payload), nicht ambient Kontext.
|
||
|
||
```ts
|
||
type Actor =
|
||
| { kind: 'user' }
|
||
| { kind: 'ai'; missionId; iterationId; rationale }
|
||
| { kind: 'system'; source: 'projection' | 'rule' | 'migration' };
|
||
```
|
||
|
||
- **Events**: `EventMeta.actor: Actor` (required — kein Legacy-Fallback)
|
||
- **Records**: Dexie-Hooks stempeln `__lastActor` + feldweise `__fieldActors`
|
||
(parallel zu `__fieldTimestamps`)
|
||
- **Sync-Payload**: `_pendingChanges.actor` geht an mana-sync (Go/Postgres-Migration offen)
|
||
- **Ambient-Hilfe**: `runAs(actor, fn)` an definierten Boundaries — Primitive frieren
|
||
den Actor synchron ein, bevor er ueber `setTimeout` / `queueMicrotask` verloren geht
|
||
|
||
Code: `apps/mana/apps/web/src/lib/data/events/actor.ts`
|
||
|
||
### 20.2 Policy-Layer
|
||
|
||
AI-Writes werden nicht automatisch ausgefuehrt. Per-Tool-Policy entscheidet:
|
||
|
||
| Decision | Bedeutung |
|
||
|----------|-----------|
|
||
| `auto` | Direkt ausfuehren, Actor in Events + Records stempeln |
|
||
| `propose`| Als Proposal in `pendingProposals` stagen, User approved inline |
|
||
| `deny` | Refuse — Tool niemals fuer AI zugaenglich |
|
||
|
||
Default (`DEFAULT_AI_POLICY`): lesendes / append-only self-state → `auto`, alles Mutierende
|
||
→ `propose`. User / System Actors umgehen die Policy.
|
||
|
||
Code: `apps/mana/apps/web/src/lib/data/ai/policy.ts`
|
||
|
||
### 20.3 Proposals
|
||
|
||
```ts
|
||
interface Proposal {
|
||
id, createdAt, expiresAt?, status: 'pending' | 'approved' | 'rejected' | 'expired';
|
||
actor: { kind: 'ai', missionId, iterationId, rationale };
|
||
missionId?, iterationId?; // fuer Workbench-Queries indiziert
|
||
intent: { kind: 'toolCall', toolName, params };
|
||
decidedAt?, decidedBy?, userFeedback?;
|
||
}
|
||
```
|
||
|
||
Proposals sind **lokal only** — sie syncen nicht. Der approved Write syncet
|
||
normal durch den Modulpfad, mit dem AI-Actor attribuiert.
|
||
|
||
Approval-Flow: `approveProposal(id)` laeuft das gespeicherte Intent unter
|
||
`runAsAsync(aiActor, () => executeToolRaw(...))`. `executeToolRaw` umgeht
|
||
die Policy — sonst wuerde sie das Intent sofort wieder in ein Proposal
|
||
zurueckwerfen.
|
||
|
||
Code: `apps/mana/apps/web/src/lib/data/ai/proposals/`
|
||
|
||
### 20.4 Ghost-UI in Pilot-Modul (todo)
|
||
|
||
`<AiProposalInbox module="todo" />` ist die **opt-in Komponente**: pro Modulseite
|
||
ein Einzeiler. Rendert pending Proposals als dashed Ghost-Karten ueber dem echten
|
||
Content — zero UI wenn keine anstehen. Approve / Reject inline. Filter ueber
|
||
Tool-Registry: Proposal fuer `create_task` landet auf `/todo`, `create_event` auf
|
||
`/calendar`, etc.
|
||
|
||
Code:
|
||
- `apps/mana/apps/web/src/lib/components/ai/AiProposalInbox.svelte`
|
||
- `apps/mana/apps/web/src/lib/data/ai/proposals/queries.ts`
|
||
|
||
### 20.5 Roadmap
|
||
|
||
- [x] Schritt 1 — Actor-Attribution (Events + Records + Sync-Payload)
|
||
- [x] Schritt 2 — Policy-Config + `pendingProposals` + Propose-Path im Executor
|
||
- [x] Schritt 3 — Ghost-UI im Todo-Pilot (`<AiProposalInbox />`)
|
||
- [x] Schritt 4 — Missions-Datenmodell + Planner-LLM-Task
|
||
- Dexie `aiMissions` (v18), cross-device synct
|
||
- `aiPlanTask` als LlmTask (minTier browser, contentClass personal)
|
||
- Strikter JSON-Parser mit Tool-Allowlist + Rationale-Zwang
|
||
- [x] Schritt 5 — In-App MissionRunner (Foreground-Tick in `(app)/+layout.svelte`)
|
||
- `runMission(id, deps)` + `runDueMissions(now, deps)` — injiziert, testbar
|
||
- Default-Input-Resolver für Notes / Kontext / Goals
|
||
- 60-Sekunden-Tick, Overlap-Guard, idempotent
|
||
- [x] Schritt 6 — Missions-UI unter `/companion/missions`
|
||
- Create-Form mit Konzept-Markdown + Objective + Cadence-Picker
|
||
- List / Detail mit pause / resume / complete / delete / "Jetzt ausführen"
|
||
- Iteration-History + pro-Iteration Freitext-Feedback-Textarea
|
||
- `<MissionInputPicker>`-Komponente mit Indexer-Registry, Default-Indexer
|
||
für notes / kontext / goals (symmetrisch zu den Resolvern)
|
||
- [x] Schritt 7 — Workbench-Timeline-Lens unter `/companion/workbench`
|
||
- Live-Query `_events` gefiltert auf `actor.kind === 'ai'`
|
||
- `bucketByIteration` gruppiert Events pro Mission-Iteration, Rationale
|
||
einmal pro Bucket statt pro Event
|
||
- Filter: Mission (per query-string) + Modul (dropdown), Deep-Link ins
|
||
Modul pro Event
|
||
- **Revert-per-Iteration**: Button pro Bucket, `data/ai/revert/`
|
||
Registry mit Inverse-Ops für TaskCreated/Completed, CalendarEvent-
|
||
Created, PlaceCreated, DrinkLogged. Newest-first Reihenfolge,
|
||
RevertStats-Summary ("X zurückgenommen · Y nicht unterstützt").
|
||
- [x] Schritt 7a — System-Actor-Wrapping für Projections (streaks-Tracker)
|
||
- [x] Schritt 8 — mana-sync Go + Postgres-Migration für `actor`-Feld
|
||
- `sync_changes.actor JSONB` Column (idempotent `ADD COLUMN IF NOT EXISTS`)
|
||
- `Change.Actor json.RawMessage` Wire-Shape, opaque Server-seitig
|
||
- `RecordChange` + alle drei SELECT-Pfade (GetChangesSince / GetAll /
|
||
StreamAllUserChanges) lesen/schreiben Actor
|
||
- Webapp-Parität: `SyncChange.actor?` + Push-Payload + `applyServerChanges`
|
||
stempelt `__lastActor` + `__fieldActors` aus eingehenden Changes
|
||
→ **cross-device Attribution geschlossen**
|
||
- [x] Schritt 9 — Server-side `mana-ai` Bun-Service (v0.3, Close-the-Loop)
|
||
- `services/mana-ai (3067
|
||
- `@mana/shared-ai` Package als Single-Source-of-Truth für Planner-
|
||
Prompt + Parser + Typen (Webapp + Service importieren identisch)
|
||
- Field-level LWW-Replay von `sync_changes` in `db/missions-projection.ts`
|
||
- Tick-Loop: Due Missions → Planner → mana-llm → Parse → Write-Back
|
||
- `db/iteration-writer.ts` appendet Server-Iteration via RLS-scoped
|
||
`withUser` Transaktion mit Actor `{kind:'system', source:'mission-runner'}`
|
||
- Webapp-Staging-Effect (`server-iteration-staging.ts`) übersetzt
|
||
eingehende `source:'server'` Iterationen in lokale Proposals pro
|
||
PlanStep mit AI-Actor-Attribution; idempotent via proposalId-Marker
|
||
- Server-side Input-Resolver (`db/resolvers/`) für plaintext Tabellen
|
||
(goals); encrypted Tables bleiben privacy-by-design browser-only
|
||
- Contract-Test via `@mana/shared-ai`'s `AI_PROPOSABLE_TOOL_NAMES` +
|
||
Runtime-Drift-Guard im Service
|
||
- `mana_ai.mission_snapshots` — inkrementeller Snapshot, `listDueMissions`
|
||
ist ein indexed SELECT statt O(N) LWW-Replay
|
||
- **Observability**: `/metrics` (Prom-Counter für ticks/plans/parse-
|
||
failures/snapshots + Histogramme für tick-, planner-, HTTP-Latenz)
|
||
scraped vom `mana-ai`-Job in `docker/prometheus/prometheus.yml`.
|
||
`/health` als blackbox-internal Probe → surfaces auf **status.mana.how**
|
||
als „Mana AI Runner".
|
||
|
||
**Die Workbench-Roadmap ist damit funktional abgeschlossen.**
|
||
|
||
### 20.5a Symmetrische Registries: Resolver vs. Indexer
|
||
|
||
Zwei parallele Module-Opt-in-Points für die AI-Layer:
|
||
|
||
| Registry | Richtung | Nutzer | Beispiel |
|
||
|----------|----------|--------|----------|
|
||
| `input-resolvers.ts` | `Ref → Prompt-Text` | Runner (Planner-Kontext) | `notes/abc-123 → "Titel\nContent…"` |
|
||
| `input-index.ts` | `Module → Candidates[]` | UI (Picker) | `notes → [{label:"Idee", hint:"…"}]` |
|
||
|
||
Beide werden im selben `default-resolvers.ts` zusammen registriert, damit die Paare synchron bleiben. Neues Modul anbinden = `registerInputResolver(name, resolver) + registerInputIndexer(name, indexer)` — keine Änderungen am AI-Core nötig.
|
||
|
||
### 20.6 Offene Follow-ups
|
||
|
||
- **mana-sync (Go) + Postgres-Migration** fuer `actor`-Feld im pendingChange-Payload
|
||
- **System-Actor** in Projections + Rule-Engine wrappen (heute im User-Kontext)
|
||
- **Inbox-Rollout** auf weitere Module (Kalender, Notes, …) sobald Tools dort
|
||
in `DEFAULT_AI_POLICY` eingetragen sind
|
||
|
||
### 20.7 Manueller Test
|
||
|
||
Browser-Console auf `/todo`:
|
||
|
||
```js
|
||
const { executeTool } = await import('/src/lib/data/tools/executor');
|
||
await executeTool(
|
||
'create_task',
|
||
{ title: 'Test von der KI' },
|
||
{ kind: 'ai', missionId: 'demo', iterationId: '1', rationale: 'Beispiel-Proposal' }
|
||
);
|
||
```
|
||
|
||
Ghost-Karte erscheint sofort ueber der Task-Liste.
|
||
|
||
## 21. Mission Key-Grant (ab 2026-04-15, in Arbeit)
|
||
|
||
Opt-in Mechanismus der es `mana-ai` erlaubt, Missions auf **encrypted** Tabellen (notes, tasks, events, journal, kontext) serverseitig auszufuehren — ohne dass ein Browser-Tab des Users offen sein muss. Ohne Grant bleibt der Foreground-Runner zustaendig; das ist der Default und aendert sich nicht.
|
||
|
||
Vollstaendiger Plan: [`docs/plans/ai-mission-key-grant.md`](../plans/ai-mission-key-grant.md). Ideen-Kontext: [`docs/future/AI_AGENTS_IDEAS.md`](../future/AI_AGENTS_IDEAS.md#1-encrypted-tables-serverseitig-nutzbar-machen).
|
||
|
||
### Flow
|
||
|
||
1. **Consent** — User aktiviert Mission mit encrypted Input → `MissionGrantDialog` erklaert Record-Scope und TTL, fragt explizit um Erlaubnis. Zero-Knowledge-User sehen den Dialog nicht; Grant ist dort hart deaktiviert.
|
||
2. **Derivation** — Webapp ruft `deriveMissionDataKey(masterKey, { version, missionId, tables, recordIds })` aus `@mana/shared-ai`. HKDF-SHA256 mit `missionId` als Salt; Scope-Binding im `info`-String → jede Scope-Aenderung erzeugt kryptografisch einen anderen Key, alte Grants werden automatisch ungueltig.
|
||
3. **Wrap** — `mana-auth` `POST /me/ai-mission-grant` wrappt den MDK mit dem RSA-OAEP-2048 Public-Key von `mana-ai` (aus `MANA_AI_PUBLIC_KEY_PEM`). Antwort: `{ wrappedKey, derivation, issuedAt, expiresAt }` → Webapp schreibt das als `Mission.grant`.
|
||
4. **Sync** — Grant-Blob fliesst ueber das normale Sync-System an `mana_sync`. Keine Sonderbehandlung — `wrappedKey` ist bereits RSA-geschuetzt.
|
||
5. **Unwrap** — `mana-ai` holt beim Tick den privaten Key (`MANA_AI_PRIVATE_KEY_PEM`), entwrappt den MDK nur im Prozessspeicher, liest allowlisted Records aus `sync_changes`, entschluesselt, plant.
|
||
6. **Audit** — jede Entschluesselung schreibt eine Zeile in `mana_ai.decrypt_audit` (RLS-scoped auf `app.current_user_id`). User kann das in der Workbench unter "Mission -> Datenzugriff" einsehen.
|
||
7. **Lifecycle** — Grant hat Default-TTL 7 Tage rollend. Revoke via Workbench-Button → `grant=null`, Mission pausiert (`state='grant-revoked'`). Abgelaufen → Runner ueberspringt mit `state='grant-missing'`, Foreground-Runner uebernimmt beim naechsten Tab-Open.
|
||
|
||
### Komponenten (Status)
|
||
|
||
| Komponente | Wo | Status |
|
||
|---|---|---|
|
||
| Canonical HKDF + Types | `packages/shared-ai/src/missions/grant.ts` | done (Phase 1a) |
|
||
| `Mission.grant` Feld | `packages/shared-ai/src/missions/types.ts` | done |
|
||
| `mana_ai.decrypt_audit` + RLS | `services/mana-ai/src/db/migrate.ts` | done (Phase 1b) |
|
||
| `MANA_AI_PUBLIC_KEY_PEM` / `MANA_AI_PRIVATE_KEY_PEM` config | auth + ai configs | done (Phase 0) |
|
||
| `POST /me/ai-mission-grant` Endpoint | `services/mana-auth/src/routes/encryption-vault.ts` | Phase 1c |
|
||
| Server-side unwrap helper | `services/mana-ai/src/crypto/unwrap-grant.ts` | Phase 1d |
|
||
| Encrypted input resolver | `services/mana-ai/src/db/resolvers/encrypted.ts` | Phase 2 |
|
||
| Consent UI + Revoke | `apps/mana/apps/web/src/lib/components/ai/MissionGrantDialog.svelte` | Phase 3 |
|
||
|
||
### Privacy-Garantien
|
||
|
||
- **Zero-Knowledge-User bleiben Zero-Knowledge.** Die Webapp verweigert das Anlegen eines Grants, wenn `vault.zeroKnowledge=true`. Endpoint pruefts zusaetzlich serverseitig.
|
||
- **Kein Key-Cache.** `mana-ai` entwrappt den MDK pro Tick neu und vergisst ihn im `finally`. Minimiert RAM-Dump-Window auf die Tick-Dauer.
|
||
- **Scope-Verletzung = Crypto-Failure.** Record-IDs sind in die Key-Derivation gebunden. Runtime-Allowlist-Check ist belt+braces, nicht die alleinige Verteidigung.
|
||
- **Keine Write-Grants.** Server staget nur Proposals; User genehmigt wie bisher. Grant = read-only.
|
||
|
||
### Nicht-Ziele
|
||
|
||
- Cross-User-Missions (pro Grant genau ein User).
|
||
- Automatische Key-Rotation (Master-Key-Rotation invalidiert alle Grants → User re-consented beim naechsten Edit).
|
||
- Grant-Sync-Konflikte (werden ueber normales LWW aufgeloest; bei Scope-Mismatch wirft der Resolver `scope-violation` und die Mission pausiert).
|
||
|
||
## 22. Multi-Agent Workbench (ab 2026-04-15)
|
||
|
||
Upgrade von Single-User, Single-AI ("Mana") zu Single-User, Multi-Agent. Missionen gehoeren jetzt einem benannten Agent; Scenes koennen als Lens an einen Agent gebunden werden. Full context + decisions: [`docs/plans/multi-agent-workbench.md`](../plans/multi-agent-workbench.md).
|
||
|
||
### Datenmodell
|
||
|
||
```
|
||
Agent {
|
||
id, name (unique per user), avatar, role,
|
||
systemPrompt, memory, // encrypted at rest
|
||
policy: AiPolicy, // per-tool + per-module + global default
|
||
maxConcurrentMissions, maxTokensPerDay,
|
||
state: active|paused|archived
|
||
}
|
||
|
||
Mission.agentId?: string // owning agent; legacy records backfill-stamped
|
||
WorkbenchScene.viewingAsAgentId?: string // UI lens, does not affect data scope
|
||
|
||
Actor {
|
||
kind: 'user' | 'ai' | 'system',
|
||
principalId: string, // userId | agentId | 'system:<source>'
|
||
displayName: string, // cached at write time — rename doesn't rewrite history
|
||
// AI-only:
|
||
missionId?, iterationId?, rationale?
|
||
}
|
||
```
|
||
|
||
### Identity flow
|
||
|
||
1. User creates an agent in `/ai-agents`. Default "Mana" agent is auto-bootstrapped on first login and inherits the existing user-level policy.
|
||
2. Missions are created under an agent (`AgentPicker` in the create flow). Legacy missions were backfilled to the default agent via localStorage-sentinelled one-shot migration.
|
||
3. `executor.executeTool` loads `getAgent(actor.principalId).policy` for every AI write; `mana-ai` does the same server-side via `agent_snapshots` (LWW projection mirroring `mission_snapshots`).
|
||
4. Every write stamps `Actor.displayName = agent.name` at write time. Workbench timeline + Revert remain correct even after the agent is renamed or deleted (ghost-agent marker on tab).
|
||
5. `mana-ai` tick filters `AI_AVAILABLE_TOOLS` by agent policy before the prompt, injects plaintext `systemPrompt + memory` in an `<agent_context>` delimiter block (ciphertext fields stay server-invisible by design — foreground runner picks up encrypted context).
|
||
6. Scenes optionally bind `viewingAsAgentId`. Pure UI lens: SceneAppBar shows the agent avatar on the scene tab, Workbench timeline defaults its filter to that agent. No data-scope change.
|
||
|
||
### Gate order in the server tick
|
||
|
||
Before an LLM call even happens:
|
||
- `agent.state === 'archived'` → skip silently, bump `agentDecisionsTotal{decision='skipped-archived'}`
|
||
- `agent.state === 'paused'` → same with `skipped-paused`
|
||
- `activeRuns[agentId] >= agent.maxConcurrentMissions` → `skipped-concurrency`, defer to next tick
|
||
- Otherwise → `ran`
|
||
|
||
Missions without an owning agent don't produce this metric; `plansWrittenBackTotal` is the universal "did we run" counter.
|
||
|
||
### Scene-Agent binding semantics
|
||
|
||
`scene.viewingAsAgentId` is a **lens**, not a scope. The open apps in the scene still read the same user data regardless of which agent (if any) is bound. The binding drives:
|
||
- Agent avatar on the scene tab (SceneAppBar)
|
||
- Default `agent` filter in the AI Workbench timeline
|
||
- Default selected agent when creating a mission from a bound scene (future — Phase 8 polish)
|
||
|
||
This is deliberately orthogonal: one agent can appear in many scenes; one scene can be unbound ("neutral workspace"). Binding is set/changed via the scene context menu → "An Agent binden…" dialog.
|
||
|
||
### Nicht-Ziele
|
||
|
||
- **Kein Agent-to-Agent Messaging.** Agents laufen unabhaengig.
|
||
- **Kein Meta-Planner ueber Agents.** Agents erzeugen sich keine Missionen selbst; der User bleibt Mission-Creator.
|
||
- **Keine Team-Features.** Andere User / geteilte Daten kommen in einem separaten Plan nach dieser Iteration.
|
||
- **Keine Agent-Memory-Self-Modification.** Memory wird nur vom User editiert.
|
||
- **Keine Per-Agent-Encryption-Domains.** Alle Agents sehen alle Daten des einen Users. Mission-Key-Grants bleiben per-Mission.
|
||
|
||
## 23. Reasoning Loop + Research Pre-Step + Debug Log (ab 2026-04-15)
|
||
|
||
### 23.1 Reasoning Loop
|
||
|
||
The foreground Runner (`apps/mana/apps/web/src/lib/data/ai/missions/runner.ts`) wraps the plan→stage pipeline in a loop of up to `MAX_REASONING_LOOP_ITERATIONS` (5) rounds per iteration:
|
||
|
||
```
|
||
while (budget remaining):
|
||
plan = planner(mission, loopInputs, availableTools)
|
||
if plan.steps == 0 → break (agent done)
|
||
for each step:
|
||
resolve policy
|
||
auto → execute inline, collect {autoData, autoMessage}
|
||
propose → stage proposal, set humanInLoop=true
|
||
fail → record step as failed
|
||
if humanInLoop → break (wait for user approval)
|
||
if no auto-outputs → break (no progress)
|
||
loopInputs += synthetic ResolvedInput("Zwischenergebnisse Runde N",
|
||
formatted tool outputs as JSON fenced blocks)
|
||
```
|
||
|
||
This enables read→reason→act missions ("list notes → tag each one") in a single user-triggered run. The `StageOutcome` type carries `autoData` + `autoMessage` so auto-executed tool payloads thread back into the prompt without a second executor call.
|
||
|
||
**Budget**: 5 loop iterations = 5 LLM calls max. Planner `maxTokens` raised to 4096 to accommodate batch output (up to ~15 step objects). System prompt teaches the planner about the loop: "read-only tools auto-execute, write-tools get staged, emit all batch writes in one plan because staging ends the turn."
|
||
|
||
### 23.2 Research Pre-Step
|
||
|
||
Before the planner runs, if the mission objective matches `/recherchier|research|news|finde|suche|aktuelle|neueste/i`, the Runner calls the `news-research` module's RSS pipeline:
|
||
|
||
1. `discoverByQuery(objective, lang)` — finds matching RSS feeds
|
||
2. `searchFeeds(feedUrls, objective, {limit: 10})` — ranks articles by relevance
|
||
3. Results formatted as a `ResolvedInput` with explicit instructions ("für jeden relevanten Artikel rufe `save_news_article(url)` auf")
|
||
|
||
Chosen over the deep-research pipeline (`/api/v1/research/start-sync`) because: no credits consumed, faster (~2s vs ~12s), no SearXNG dependency, uses own RSS infrastructure. The deep-research pipeline still exists for the questions module.
|
||
|
||
Failures throw explicitly (0 feeds or 0 articles) — the runner catches and injects a "research failed" `ResolvedInput` with the error message so the planner doesn't hallucinate URLs.
|
||
|
||
#### Server-Side Research (mana-ai, ab v0.6)
|
||
|
||
The `mana-ai` background runner mirrors the client-side research pre-step. `NewsResearchClient` (`services/mana-ai/src/planner/news-research-client.ts`) calls `mana-api`'s `POST /api/v1/news-research/discover` + `/search` directly over the Docker network (`MANA_API_URL`). The same `RESEARCH_TRIGGER` regex is used; when it matches, results are injected as `ResolvedInput { id: '__web-research__' }` before the planner prompt is built.
|
||
|
||
Key differences from the client-side path:
|
||
- No SvelteKit context — pure HTTP fetch.
|
||
- 15s timeout on discover, 30s on search (tighter than client because the tick has a 60s cadence).
|
||
- Graceful null on any failure — the planner just runs without research context; tick doesn't crash.
|
||
- No `discoverByQuery` / `searchFeeds` module imports — the client ships those from `@mana/shared-rss`; the server calls the API endpoints which wrap the same logic.
|
||
|
||
This makes the Recherche-Agent and Today-Agent fully autonomous (no browser tab required). `research_news` is also registered as a proposable tool so the planner can explicitly request additional research as a PlanStep.
|
||
|
||
### 23.3 Kontext Auto-Inject
|
||
|
||
The user's `kontextDoc` singleton is automatically appended to every planner call as a standing-context `ResolvedInput`, unless the mission already links it as an explicit input. Decrypted client-side only — the server-side mana-ai runner skips this (encryption barrier; needs a Key-Grant for server access).
|
||
|
||
### 23.4 Debug Log
|
||
|
||
Per-iteration diagnostic capture in local-only Dexie table `_aiDebugLog` (schema v20, never synced — contains decrypted prompt content). Keyed by `iterationId`, capped at 50 rows.
|
||
|
||
**Captured per iteration:**
|
||
- `plannerCalls[]` — array (one per loop round): `{systemPrompt, userPrompt, rawResponse, latencyMs}`
|
||
- `loopSteps[]` — auto-executed tool log: `{loopIndex, toolName, params, outputPreview}`
|
||
- `preStep` — web-research outcome or kontext injection state
|
||
- `resolvedInputs[]` — full list the planner saw (grows across loop rounds)
|
||
|
||
**UI:** `<AiDebugBlock iterationId={id}>` renders an expandable panel under each iteration card:
|
||
- Summary chip: "2× LLM · 4200ms · 1× Auto-Tool"
|
||
- Collapsible sections: Pre-Step, Resolved Inputs (each individually expandable), Auto-Tool outputs, per-LLM-call prompt+response
|
||
- "📋 JSON" button copies the entire debug entry to clipboard
|
||
|
||
**Toggle:** `localStorage.setItem('mana.ai.debug', '1')`. Defaults to enabled in DEV builds, disabled in production. Checkbox in mission-detail header exposes the toggle without DevTools.
|
||
|
||
### 23.5 New Proposable Tools
|
||
|
||
| Tool | Module | Policy | Purpose |
|
||
|------|--------|--------|---------|
|
||
| `save_news_article` | news | propose | Save URL to reading list via Readability extract |
|
||
| `list_notes` | notes | auto | List notes (id, title, excerpt) for planner context |
|
||
| `update_note` | notes | propose | Full overwrite of title/content (destructive) |
|
||
| `append_to_note` | notes | propose | Append text to end of note (non-destructive) |
|
||
| `add_tag_to_note` | notes | propose | Append `#Tag` idempotently (deduplicates, case-insensitive) |
|
||
|
||
All propose-tools registered in `@mana/shared-ai` `AI_PROPOSABLE_TOOL_NAMES` and mirrored in `services/mana-ai/src/planner/tools.ts` (boot-time drift guard). `AiProposalInbox` mounted on `/news` and `/notes` pages.
|