Melian

The Voice in the Trees

The voice that speaks from the branches, choosing words, choosing tools, choosing when to act and when to listen.

Overview

At her core, Melian is an agent: a loop that receives input, decides what to do, uses tools, and responds. The agent router orchestrates this cycle, managing tool calls, streaming responses, and maintaining conversational state across all integrations.

The Router

The agent router is the main loop. It takes a user message, constructs the full prompt (system instructions + core memory + conversation history), calls the LLM, and processes the response, including any tool calls.

Tool calls are executed in sequence. Each tool returns a result that feeds back into the conversation, allowing Melian to chain actions: search memory, read an email, check the calendar, and synthesize a response. The router streams chunks as they arrive, yielding typed events the caller can consume.

export interface AgentRouterConfig {
  llm: LLMProvider;
  tools: ToolRegistry;
  coreMemory: CoreMemory;
  recall: RecallMemory;
  personality: PersonalityLoader;
  spirit: SpiritLoader;
  maxContextTokens: number;
  metrics?: MetricsCollector;
  user?: { name: string; interests?: string[] };
}

Streaming Response

The router yields a typed stream rather than a single response. Consumers receive chunks as the LLM and tools produce them:

export type RouterChunk =
  | { type: "text"; content: string }
  | { type: "tool_call"; name: string; args: Record<string, any> }
  | { type: "tool_result"; name: string; result: string }
  | { type: "spirit"; action: "formed" | "reinforced"; category: string }
  | { type: "done" };

Chat messages in the conversation thread follow the OpenAI-compatible format:

export interface ChatMessage {
  role: "system" | "user" | "assistant" | "tool";
  content: string | null;
  tool_call_id?: string;
  tool_calls?: ToolCall[];
}

Tool Registry

Tools are registered as ToolDefinition objects and loaded into the LLM's tool list on each turn:

export interface ToolDefinition {
  type: "function";
  function: {
    name: string;
    description: string;
    parameters: Record<string, unknown>;
  };
}

Melian has 50+ tools across these categories:

Category Count Examples
Memory 8 core_memory_read, core_memory_update, core_memory_clear, archival_memory_insert, archival_memory_search, archival_memory_delete, recall_memory_search, recall_memory_list
Spirit 1 spirit_evolve
Jobs 4 create_job, list_jobs, delete_job, pause_job
Email 7 email_list_unread, email_search, email_read, email_check, email_send, email_approve_draft, email_discard_draft
iMessage 2 imessage_send, imessage_history
Tasks 6 tasks_list, tasks_add, tasks_complete, tasks_search, tasks_backlog, tasks_unbacklog
Calendar 8 calendar_list_events, calendar_get_event, calendar_find_free_time, calendar_create_event, calendar_update_event, calendar_delete_event, calendar_approve_draft, calendar_reject_draft
Browser 7 browser_open, browser_snapshot, browser_click, browser_fill, browser_extract_text, browser_screenshot, browser_close
Vision 1–3 ocr_image, remarkable_ocr (conditional), remarkable_view (conditional)
Smart Home / Wiz 4 wiz_discover, wiz_register_device, wiz_control, wiz_list_devices
Smart Home / Govee 5 govee_discover, govee_register_device, govee_control, govee_list_devices, govee_read_sensor
Knowledge 4 knowledge_save, knowledge_search, knowledge_list, knowledge_delete
Shell / File 2 shell_exec, write_file
Dev 2 claude_code, project_create

Model Routing

Melian routes each request to a light or heavy model based on a complexity score from 0 to 1. The score is computed before the LLM call:

Signal Weight added
Message length > 500 chars +0.20
Non-system message count > 5 +0.15
Active tool calls present +0.15
More than one question mark +0.10
Code blocks in the message +0.15
System prompt length > 2048 characters +0.10

When the score meets or exceeds the configured threshold, the request routes to the heavy model. Simple queries stay on the fast path for lower latency and cost.

Spirit & Personality

Melian's personality is not static. The spirit_evolve tool updates her character over time based on observed interactions. Each spirit entry tracks a trait, a category, its strength on a 1–10 scale, and when it was first formed and last reinforced:

export interface SpiritEntry {
  trait: string;
  category: string;
  strength: number;      // 1–10
  formed: string;        // ISO date YYYY-MM-DD
  reinforced: string;    // ISO date YYYY-MM-DD
}

When spirit_evolve fires, the new trait embedding is compared against existing entries using cosine similarity. A score ≥ 0.85 reinforces the existing entry rather than creating a duplicate. The spirit store is capped at 100 entries; when the cap is exceeded, the entry with the lowest retention score (a function of strength, recency, and reinforcement frequency) is pruned.

Personality and spirit are loaded alongside core memory at router startup and injected into the system prompt, shaping every response Melian gives.