Skip to content

@ludus/commentator

AI commentary pipeline for Ludus games. Classifies events by significance, filters through persona thresholds, generates narration, and delivers via WebSocket or Telegram.

Terminal window
npm install @ludus/commentator
import { LiveCommentaryManager, ORIANA } from '@ludus/commentator';
import { GameEventEmitter } from '@ludus/game-engine';
const emitter = new GameEventEmitter();
const manager = new LiveCommentaryManager(emitter, ORIANA);
manager.subscribe((commentary) => {
console.log(`[${commentary.persona}] ${commentary.text}`);
});
// Game runs, events fire, commentary arrives automatically

Four stages: classify → filter → narrate → summarize.

import { CommentaryPipeline, EventClassifier, ContextWindowManager, StubNarrator, ORIANA } from '@ludus/commentator';
const pipeline = new CommentaryPipeline(
new StubNarrator(ORIANA), // Or an LLM-backed narrator
new EventClassifier(),
new ContextWindowManager(),
ORIANA,
);
const commentary = await pipeline.processEvent(gameEvent);
// Commentary | null (null if filtered)

Scores events 0–1 and assigns significance levels:

EventDefault ScoreLevel
game_over1.0critical
player_eliminated0.95critical
game_start0.9critical
turn_timeout0.6high
action_executed0.4medium
invalid_action0.2low
turn_skipped0.15low

Actions with captures or eliminations get +0.3 boost; critical flag adds +0.2.

import { scoreToLevel, shouldNarrate } from '@ludus/commentator';
scoreToLevel(0.9); // "critical"
shouldNarrate(classifiedEvent, 0.4); // true if score >= threshold

Sliding window with automatic compaction:

import { ContextWindowManager } from '@ludus/commentator';
const ctx = new ContextWindowManager({ maxRecentEvents: 8, maxEstimatedTokens: 2000 });
ctx.pushEvent(event);
const context = ctx.getContext(); // { recentEvents, eventSummary, turnNumber, estimatedTokens }

Interface for generating narration text:

interface NarratorProvider {
narrate(event: ClassifiedEvent, context: CommentaryContext): Promise<string>;
summarize(result: GameResult, context: CommentaryContext): Promise<GameSummary>;
getProviderId(): string;
}

StubNarrator provides template-based narration for testing (no LLM required).

PersonaGameToneThresholdVerbosity
ORIANAKonquistaDramatic, imperial0.4Verbose
URBANODestreectProfessional, analytical0.6Balanced
import { ORIANA, URBANO, PERSONA_PRESETS } from '@ludus/commentator';
import type { CommentatorPersona } from '@ludus/commentator';
const myPersona: CommentatorPersona = {
id: 'tacticus',
name: 'TACTICUS',
gameId: 'chess',
description: 'Analytical chess commentator',
voice: { tone: 'analytical', vocabulary: ['gambit', 'fork', 'pin'], catchphrases: ['Brilliant move!'] },
biases: { patience: 0.9, creativity: 0.3 },
narrationThreshold: 0.5,
verbosity: 'terse',
systemPromptTemplate: 'You are {{name}}, a {{tone}} chess commentator...',
};
import { PersonaRegistry } from '@ludus/commentator';
const registry = new PersonaRegistry(); // ORIANA + URBANO auto-registered
registry.register(myPersona);
registry.get('tacticus'); // CommentatorPersona
registry.list(); // All registered personas

LiveCommentaryManager wires the pipeline into a game’s event emitter:

const manager = new LiveCommentaryManager(emitter, ORIANA);
const unsub = manager.subscribe(callback);
manager.getHistory(); // Commentary[] — for late joiners
manager.personaId; // "oriana"
manager.dispose(); // Clean up

Subscribes to LiveCommentaryManager, filters to HIGH + CRITICAL events, and delivers to Telegram subscribers.

import { TelegramDeliverySink, SubscriptionService, InMemorySubscriptionStore, StubTelegramClient } from '@ludus/commentator';
const store = new InMemorySubscriptionStore();
const subService = new SubscriptionService(store);
const client = new StubTelegramClient(); // Or a real Telegram Bot API client
const sink = new TelegramDeliverySink(manager, client, subService, {
gameId: 'game-123',
gameType: 'konquista',
});
sink.start();
// Commentary flows → filtered → formatted → delivered to subscribers
sink.signalGameOver(); // Starts 5-min cooldown
sink.dispose();
  • Critical events: 🔥 Turn N narration text
  • High events: ⚔️ Turn N narration text

Handles Telegram bot commands:

const result = await subService.handleCommand('chat-123', 'user-1', { command: 'follow', agentName: 'Caesar' });
// { reply: "Subscribed to agent Caesar.", success: true }
const subscribers = await subService.getSubscribersForGame('konquista', ['Caesar', 'Brutus']);
import { RateLimiter, DEFAULT_RATE_LIMITS } from '@ludus/commentator';
const limiter = new RateLimiter(); // Uses defaults
limiter.canSend('chat-1', 'game-1'); // true/false
limiter.record('chat-1', 'game-1'); // Record a send
limiter.remainingForSubscriber('chat-1', 'game-1'); // Remaining allowance
limiter.startCooldown('game-1'); // Post-game cooldown

Default limits: 30 msg/sec global, 20/game per subscriber, 1s batch delay, 5-min post-game cooldown.

// Core
import type { Commentary, CommentaryCallback, CommentaryContext, ClassifiedEvent,
SignificanceLevel, EventCategory, CommentatorPersona, CommentatorVoice,
Verbosity, GameSummary, NarratorProvider, PipelineStage } from '@ludus/commentator';
// Telegram
import type { TelegramSubscription, SubscriptionType, SubscriptionStore,
TelegramClient, TelegramMessage, TelegramBotCommand, RateLimitConfig,
TelegramDeliverySinkConfig, FormattedCommentary, CommandResult } from '@ludus/commentator';