From 4192a4bd9bd87ed64011f7cbbbc6ed45558dc03c Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 14 Apr 2026 13:54:43 +0200 Subject: [PATCH] feat(brain): emit Companion chat + tool events for observability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the gap where the Companion module wrote messages directly to IndexedDB without participating in the Domain Event stream — chat activity, tool invocations and conversation creation were invisible to the Event Stream page, Goals, Streaks, and Memory layer. New events (3 types): - CompanionConversationStarted: emitted on chatStore.createConversation - CompanionMessageSent: emitted on user/assistant messages (skips empty tool plumbing messages) - CompanionToolCalled: emitted in engine.runCompanionChat after every tool execution, with tool name, source module, success/failure, latency in ms, and error message on failure Event Stream page updated with icons (ChatCircle, Robot) and German labels for the three new event types so they appear inline with all other domain activity. Now possible (future iterations): - Goals like "5x Companion-Chat pro Woche" - Streaks for daily Companion usage - Tool performance analytics ("create_task hat 3% Fehlerrate") - Memory facts about which tools the user uses most Total: 70 event types, 51 tools across 31 modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/data/events/catalog.ts | 32 +++++++++++++++++++ .../web/src/lib/modules/companion/engine.ts | 17 +++++++++- .../modules/companion/stores/chat.svelte.ts | 15 +++++++++ .../lib/modules/eventstream/ListView.svelte | 10 ++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/apps/mana/apps/web/src/lib/data/events/catalog.ts b/apps/mana/apps/web/src/lib/data/events/catalog.ts index ae42400e5..53755d47f 100644 --- a/apps/mana/apps/web/src/lib/data/events/catalog.ts +++ b/apps/mana/apps/web/src/lib/data/events/catalog.ts @@ -543,6 +543,33 @@ export interface SleepLoggedPayload { } export type SleepEventType = 'SleepLogged'; +// ── Companion (Chat + Tools) ──────────────────────── + +export interface CompanionConversationStartedPayload { + conversationId: string; + title?: string; +} + +export interface CompanionMessageSentPayload { + messageId: string; + conversationId: string; + role: 'user' | 'assistant'; + contentLength: number; +} + +export interface CompanionToolCalledPayload { + tool: string; + module: string; + success: boolean; + latencyMs: number; + errorMessage?: string; +} + +export type CompanionEventType = + | 'CompanionConversationStarted' + | 'CompanionMessageSent' + | 'CompanionToolCalled'; + // ── Body ──────────────────────────────────────────── export interface WorkoutStartedPayload { @@ -637,6 +664,7 @@ export type ManaEventType = | QuestionsEventType | MeditateEventType | SleepEventType + | CompanionEventType | SocialEventsEventType | BodyEventType | SystemEventType; @@ -742,6 +770,10 @@ export type ManaEvent = | DomainEvent<'MeditationCompleted', MeditationCompletedPayload> // Sleep | DomainEvent<'SleepLogged', SleepLoggedPayload> + // Companion + | DomainEvent<'CompanionConversationStarted', CompanionConversationStartedPayload> + | DomainEvent<'CompanionMessageSent', CompanionMessageSentPayload> + | DomainEvent<'CompanionToolCalled', CompanionToolCalledPayload> // Social Events | DomainEvent<'SocialEventCreated', SocialEventCreatedPayload> | DomainEvent<'SocialEventDeleted', SocialEventDeletedPayload> diff --git a/apps/mana/apps/web/src/lib/modules/companion/engine.ts b/apps/mana/apps/web/src/lib/modules/companion/engine.ts index 5def48188..cf19b46b2 100644 --- a/apps/mana/apps/web/src/lib/modules/companion/engine.ts +++ b/apps/mana/apps/web/src/lib/modules/companion/engine.ts @@ -11,6 +11,8 @@ import { generateContextDocument } from '$lib/data/projections/context-document' import { getToolsForLlm, executeTool } from '$lib/data/tools'; import { authStore } from '$lib/stores/auth.svelte'; import type { DaySnapshot, StreakInfo } from '$lib/data/projections/types'; +import { emitDomainEvent } from '$lib/data/events'; +import { getTool } from '$lib/data/tools/registry'; import type { LocalMessage } from './types'; import type { ToolResult } from '$lib/data/tools/types'; @@ -230,8 +232,21 @@ export async function runCompanionChat( break; } - // Execute the tool + // Execute the tool with timing + const toolStartedAt = Date.now(); const toolResult = await executeTool(toolCall.name, toolCall.params); + const toolLatencyMs = Date.now() - toolStartedAt; + + // Emit observability event for the tool call + const toolDef = getTool(toolCall.name); + emitDomainEvent('CompanionToolCalled', 'companion', 'tools', toolCall.name, { + tool: toolCall.name, + module: toolDef?.module ?? 'unknown', + success: toolResult.success, + latencyMs: toolLatencyMs, + errorMessage: toolResult.success ? undefined : toolResult.message, + }); + toolCalls.push({ name: toolCall.name, params: toolCall.params, result: toolResult }); // Build response text from before/after the tool block diff --git a/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts b/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts index 01ea0df92..a1a9f9850 100644 --- a/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts @@ -7,6 +7,7 @@ */ import { db } from '$lib/data/database'; +import { emitDomainEvent } from '$lib/data/events'; import type { LocalConversation, LocalMessage } from '../types'; const CONV_TABLE = 'companionConversations'; @@ -24,6 +25,10 @@ export const chatStore = { updatedAt: now, }; await db.table(CONV_TABLE).add(conv); + emitDomainEvent('CompanionConversationStarted', 'companion', CONV_TABLE, conv.id, { + conversationId: conv.id, + title: conv.title, + }); return conv; }, @@ -68,6 +73,16 @@ export const chatStore = { updatedAt: msg.createdAt, }); + // Emit event only for actual user/assistant messages, not tool plumbing + if (role === 'user' || role === 'assistant') { + emitDomainEvent('CompanionMessageSent', 'companion', MSG_TABLE, msg.id, { + messageId: msg.id, + conversationId, + role, + contentLength: content.length, + }); + } + return msg; }, diff --git a/apps/mana/apps/web/src/lib/modules/eventstream/ListView.svelte b/apps/mana/apps/web/src/lib/modules/eventstream/ListView.svelte index 8aa6fef2c..6a181772a 100644 --- a/apps/mana/apps/web/src/lib/modules/eventstream/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/eventstream/ListView.svelte @@ -14,6 +14,8 @@ ForkKnife, MapPin, Lightning, + Robot, + ChatCircle, } from '@mana/shared-icons'; let events = $state([]); @@ -31,6 +33,9 @@ MealFromPhotoLogged: { icon: ForkKnife, color: '#F97316' }, PlaceVisited: { icon: MapPin, color: '#A855F7' }, PlaceCreated: { icon: MapPin, color: '#10B981' }, + CompanionConversationStarted: { icon: ChatCircle, color: '#8B5CF6' }, + CompanionMessageSent: { icon: ChatCircle, color: '#A78BFA' }, + CompanionToolCalled: { icon: Robot, color: '#6366F1' }, }; const EVENT_LABELS: Record) => string> = { @@ -55,6 +60,11 @@ TrackingStarted: () => 'Tracking gestartet', TrackingStopped: () => 'Tracking gestoppt', GoalReached: (p) => `Ziel erreicht: "${p.title}"`, + CompanionConversationStarted: (p) => + `Companion-Chat gestartet${p.title ? `: "${p.title}"` : ''}`, + CompanionMessageSent: (p) => + `${p.role === 'user' ? 'Du' : 'Companion'}: ${p.contentLength} Zeichen`, + CompanionToolCalled: (p) => `Tool: ${p.tool}${p.success ? '' : ' (Fehler)'} (${p.latencyMs}ms)`, }; function formatTime(iso: string): string {