Emerald is the brain and decision-making service for the Luna Protocol ecosystem. It sits between platform adapters (bots) and the Sapphire LLM gateway, handling behavior evaluation, Sapphire communication, response processing, and connection management.
Architecture:
Platform → Bot → WebSocket → Emerald → Sapphire (HTTP) → Krystal (llama.cpp)
- Bots connect to Emerald via WebSocket on port 3126
- Bots forward user messages as
MessageEvents (optionally withdebug: true) - Emerald evaluates behavior rules (burst, typo, sleep, mannerisms, voice chance)
- Emerald strips bot mentions (
@Kalupso,<@userId>) from the text - Emerald calls Sapphire's
/v1/respondvia streaming (askStream) — the response arrives token by token - On the first token, Emerald sends a
TypingCommandto the bot — the typing indicator appears immediately - Remaining tokens are buffered; when complete, Sapphire returns final metadata (text, label, emotion, debug stats)
- Emerald processes the response (applies typo/swap behavior, maps snake_case debug stats to camelCase)
- Emerald sends a
RespondCommandback to the bot withresponseText, optionalvoiceflag, and optionaldebugStats - The bot sends the response text to the platform (optionally as a voice message if
voice: true) - If the stream output was degenerate, Sapphire discards it — no respond command is sent, typing expires naturally
src/server.ts— WebSocket server that handles bot connections, message events, and sends commandssrc/brain.ts— Central decision engine: evaluates behavior rules, calls Sapphire via streaming, applies typo/swap, routes decisionssrc/sapphire-client.ts— HTTP client for communicating with Sapphire (askfor non-streaming,askStreamfor SSE streaming)src/protocol.ts— Type definitions for WebSocket messages (events & commands) includingdebug,responseText,voice,BehaviorDebugsrc/config.ts— Configuration management (YAML-based)
src/behavior/sleep.ts— Sleep schedule behaviorsrc/behavior/mannerisms.ts— Mannerism pattern injection (hesitation, burst, reactions)src/behavior/burst.ts— Burst message behaviorsrc/behavior/typo.ts— Typo/letter-swap behavior and rate limiting
src/state/state.ts— State management (activity tracking, session limits)src/state/trigger.ts— Trigger evaluation (mentions, DMs, names, keywords, random)src/state/topic-fatigue.ts— Topic fatigue tracking
Instead of waiting for the full LLM response, Emerald streams from Sapphire's SSE endpoint. The first token triggers an immediate TypingCommand to the bot — the user sees "typing..." before the model has finished generating.
All behavior decisions live in Emerald's config.yml:
- Typo chance & layout (azerty/qwerty)
- Letter swap chance
- Burst chance & delays
- Hesitation chance & word list
- Sleep schedules & timezone
- Topic fatigue thresholds
- Voice message chance (decides when to set
voice: trueon the respond command) - Forget chance
When debug: true is set on a MessageEvent, Sapphire returns token counts, timing, emotion state (valence/arousal), and classification confidence. Emerald forwards these as debugStats in the respond command. The bot appends formatted -# lines.
Bot mentions (@Kalupso, <@userId>) are stripped from the text before sending to Sapphire, preventing the model from echoing its own name.
MessageEvent—{ type: "message", id, client, channel, user, text, timestamp, isDM, mentions?, debug? }ReadyEvent—{ type: "ready", client, userId, username }BotMessageEvent—{ type: "bot_message", client, channel, text, timestamp }PresenceEvent—{ type: "presence", client, status }
RespondCommand—{ type: "respond", id, channel, text, responseText, delay, replyTo, replyStyle, hesitationWord?, burstPlan?, typoCorrection?, react?, voice?, sessionId?, debugStats? }TypingCommand—{ type: "typing", id, channel, duration }SetPresenceCommand—{ type: "set_presence", id, status, text?, activityType? }SpontaneousCommand—{ type: "spontaneous", id, channel, sessionId }ForgotCommand—{ type: "forgot", id, channel }
{
promptTokens: number;
completionTokens: number;
timeMs: number;
tokensPerSecond: number;
emotionStateValence: number;
emotionStateArousal: number;
classificationLabel: string;
classificationConfidence: number;
messageValence: number;
messageArousal: number;
behavior?: {
typoChance, typoApplied,
swapChance, swapApplied,
burstChance, burstApplied,
hesitationChance, hesitationApplied,
voiceChance, voiceApplied,
forgetChance,
sleepMode: string | null;
fatigueMultiplier: number;
};
}Copy config.example.yml to config.yml. All behavior parameters are documented in the example file.
port: 3126
sapphire_host: "127.0.0.1"
sapphire_port: 3123
sapphire_bot_username: "User"
names: ["Luna", "Pixie"]
random_chance: 0.015
# ... see config.example.yml for full options# Install
npm install
# Build (esbuild → self-cli.cjs)
npm run build
# Development
npm run dev
# Production (PM2)
npm run start