feat(brain): emit Companion chat + tool events for observability

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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-14 13:54:43 +02:00
parent 9ff2cfcdac
commit 4192a4bd9b
4 changed files with 73 additions and 1 deletions

View file

@ -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>

View file

@ -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

View file

@ -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<LocalConversation>(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;
},

View file

@ -14,6 +14,8 @@
ForkKnife,
MapPin,
Lightning,
Robot,
ChatCircle,
} from '@mana/shared-icons';
let events = $state<DomainEvent[]>([]);
@ -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, (p: Record<string, unknown>) => 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 {