The Voice in the Trees
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 |
| 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.