# Architecture **Analysis Date:** 2025-02-09 ## Pattern Overview **Overall:** Multi-channel AI agent daemon with layered pipeline architecture **Key Characteristics:** - Message pipeline: Channel Adapter → ChannelRegistry → MessageRouter → AgentOrchestrator → NativeAgent → ModelClient - Registry/factory pattern for extensible channels, tools, models, and agent configs - Dependency injection via constructor config objects (no DI container) - YAML + Zod config-driven feature toggling — most subsystems are optional and activated by config - Lifecycle-managed daemon with ordered shutdown handlers (LIFO) - SQLite for session persistence, filesystem for memory persistence, SQLite for vector embeddings ## Layers **CLI Layer:** - Purpose: Parse commands, load config, bootstrap daemon or TUI - Location: `src/cli/` - Contains: Command registrations (commander.js), config loading, TUI entry - Depends on: Config, Daemon - Used by: End user via `flynn` binary - Entry: `src/cli/index.ts` → registers commands: `start`, `tui`, `send`, `sessions`, `doctor`, `config`, `completion` **Config Layer:** - Purpose: Load YAML config, validate with Zod, expand `${ENV_VAR}` references - Location: `src/config/` - Contains: Zod schema definitions (`schema.ts`), YAML loader with env expansion (`loader.ts`) - Depends on: `zod`, `yaml` - Used by: All layers — the `Config` type flows through the entire system - Key file: `src/config/schema.ts` — single source of truth for all configuration types (409 lines) **Channel Layer:** - Purpose: Uniform messaging abstraction across platforms - Location: `src/channels/` - Contains: `ChannelAdapter` interface, `ChannelRegistry`, platform adapters - Depends on: Platform SDKs (grammy, discord.js, @slack/bolt, whatsapp-web.js) - Used by: Daemon (message routing), Automation (cron/webhook output) - Interface: `ChannelAdapter` with `connect()`, `disconnect()`, `send()`, `onMessage()` - Adapters: `src/channels/telegram/adapter.ts`, `src/channels/discord/adapter.ts`, `src/channels/slack/adapter.ts`, `src/channels/whatsapp/adapter.ts`, `src/channels/webchat/adapter.ts` **Agent Layer (Backend):** - Purpose: Core AI agent loop — process messages, execute tools, manage conversation - Location: `src/backends/native/` - Contains: `NativeAgent` (tool loop), `AgentOrchestrator` (delegation, compaction, memory) - Depends on: Models, Tools, Session, Context, Memory - Used by: Daemon (via message router), Gateway (via SessionBridge) - Key abstraction: `NativeAgent` runs the tool loop; `AgentOrchestrator` wraps it with orchestration features **Model Layer:** - Purpose: Unified interface to LLM providers with tier-based routing and fallback - Location: `src/models/` - Contains: `ModelClient` interface, provider implementations, `ModelRouter`, retry logic, cost estimation - Depends on: Provider SDKs (@anthropic-ai/sdk, openai, @google/generative-ai, ollama, @aws-sdk/client-bedrock-runtime) - Used by: Agent layer, Gateway SessionBridge - Providers: `anthropic.ts`, `openai.ts`, `gemini.ts`, `bedrock.ts`, `github.ts`, `local/ollama.ts`, `local/llamacpp.ts` **Tool Layer:** - Purpose: Tool registry, execution with policy enforcement and hook checks - Location: `src/tools/` - Contains: `ToolRegistry`, `ToolExecutor`, `ToolPolicy`, builtin tool implementations - Depends on: Hooks (confirmation), Memory, Process/Browser managers - Used by: Agent layer (tool loop), Gateway (tool execution) - Three tool patterns: - Static: `export const fooTool: Tool` (e.g., `src/tools/builtin/shell.ts`) - Factory: `export function createFooTool(dep): Tool` (e.g., `src/tools/builtin/media-send.ts`) - Multi-factory: `export function createFooTools(dep): Tool[]` (e.g., `src/tools/builtin/process/index.ts`) **Session Layer:** - Purpose: Persistent conversation history per channel+sender pair - Location: `src/session/` - Contains: `SessionStore` (SQLite), `SessionManager` (in-memory cache + store), `ManagedSession` - Depends on: `better-sqlite3` - Used by: Agent layer, Gateway SessionBridge **Memory Layer:** - Purpose: Persistent knowledge across sessions — namespace-based files + hybrid vector search - Location: `src/memory/` - Contains: `MemoryStore` (file-based), `VectorStore` (SQLite-backed embeddings), `HybridSearch`, embedding providers, text chunker - Depends on: Embedding providers (OpenAI, Gemini, Ollama, llama.cpp, Voyage) - Used by: Agent layer (memory injection into system prompt), Tools (memory.read/write/search) **Context Layer:** - Purpose: Token estimation and conversation compaction - Location: `src/context/` - Contains: Token estimator (`tokens.ts`), compaction logic (`compaction.ts`) - Depends on: Agent layer (delegation for compaction), Memory (extraction) - Used by: AgentOrchestrator (automatic compaction before each message) **Gateway Layer:** - Purpose: WebSocket JSON-RPC server + HTTP static file server + vanilla JS dashboard - Location: `src/gateway/` - Contains: `GatewayServer`, `Router`, `SessionBridge`, `LaneQueue`, auth, protocol, static serving, Tailscale Serve - Depends on: `ws`, Session, Agent, Tools - Used by: WebChat adapter, TUI (connects as WS client), external dashboard clients - Protocol: JSON-RPC 2.0 over WebSocket **Hooks Layer:** - Purpose: Pattern-based tool confirmation engine - Location: `src/hooks/` - Contains: `HookEngine` with glob-pattern matching for confirm/log/silent actions - Depends on: Nothing (pure logic) - Used by: ToolExecutor (checks before execution) **Prompt Layer:** - Purpose: Assemble system prompt from template files (SOUL.md, AGENTS.md, etc.) - Location: `src/prompt/` - Contains: Template search and assembly logic - Depends on: Filesystem (searches multiple directories) - Used by: Daemon (system prompt construction at startup) **MCP Layer:** - Purpose: Model Context Protocol server management — bridge external MCP tools into the tool registry - Location: `src/mcp/` - Contains: `McpClient`, `McpManager`, tool bridging (`bridge.ts`) - Depends on: `@modelcontextprotocol/sdk` - Used by: Daemon (starts MCP servers, registers bridged tools) **Skills Layer:** - Purpose: Pluggable skill system — bundled, managed (installed), and workspace skills - Location: `src/skills/` - Contains: `SkillRegistry`, `SkillInstaller`, skill loader - Depends on: Filesystem - Used by: Daemon (loads skills, injects into system prompt) **Agents Config Layer:** - Purpose: Named agent configurations with per-agent overrides (model tier, tools, sandbox) - Location: `src/agents/` - Contains: `AgentConfigRegistry`, `AgentRouter` (channel+sender → agent config resolution) - Depends on: Config - Used by: Daemon message router (selects agent config per session) **Automation Layer:** - Purpose: Scheduled tasks, webhooks, heartbeat monitoring, Gmail watching - Location: `src/automation/` - Contains: `CronScheduler`, `WebhookHandler`, `HeartbeatMonitor`, `GmailWatcher` - Depends on: `croner`, `googleapis`, Channels - Used by: Daemon (registers as channel adapters or standalone monitors) **Sandbox Layer:** - Purpose: Docker container isolation for tool execution - Location: `src/sandbox/` - Contains: `DockerSandbox`, `SandboxManager`, sandboxed shell/process tools - Depends on: Docker CLI - Used by: Daemon message router (replaces shell/process tools with sandboxed variants) **Frontend Layer (Legacy):** - Purpose: Direct Telegram bot integration and TUI - Location: `src/frontends/` - Contains: Telegram bot handlers with confirmation UI, TUI (minimal readline + fullscreen React/Ink) - Depends on: grammy (Telegram), ink/react (TUI) - Used by: CLI commands (`start` uses Telegram frontend, `tui` uses TUI) ## Data Flow **Inbound Message Processing (Channel → Response):** 1. Platform SDK receives message → Channel adapter normalizes to `InboundMessage` 2. Adapter calls `onMessage()` callback → `ChannelRegistry.handleInbound()` routes to `MessageHandler` 3. `createMessageRouter()` resolves agent config via `AgentRouter.resolve(channel, senderId)` 4. `getOrCreateAgent()` creates/retrieves `AgentOrchestrator` for the session (cached by `channel:sender:agentConfig`) 5. Audio routing: `supportsAudioInput()` checks provider capability — native audio passed through for Gemini/OpenAI/GitHub, transcribed via Whisper for others 6. `orchestrator.process()` → injects memory context → checks compaction → delegates to `NativeAgent.process()` 7. `NativeAgent.toolLoop()` → sends to `ModelRouter.chat()` → model returns response or tool calls 8. If tool calls: `ToolExecutor.execute()` → policy check → hook check → tool execution → loop back to model 9. Final text response returned → reply function sends via adapter → `adapter.send()` → platform SDK **Gateway WebSocket Flow:** 1. Client connects via WebSocket → auth check → `SessionBridge.connect()` → `NativeAgent` created 2. Client sends JSON-RPC message → `GatewayServer.handleMessage()` → `Router.dispatch()` → handler 3. `agent.send` handler → `LaneQueue` serializes requests → `SessionBridge` processes via `NativeAgent` 4. Streaming events sent back via WebSocket as JSON-RPC notifications 5. HTTP requests serve static dashboard UI or webhook endpoints **Model Routing with Fallback:** 1. `ModelRouter.chat(request, tier)` → tries primary client for requested tier 2. If retry config enabled: `withRetry()` wraps call with exponential backoff 3. On failure → try tier-specific fallbacks (e.g., Anthropic → GitHub Models same model) 4. On failure → try global fallback chain (typically local model) 5. All failures → throw aggregated error **Compaction Flow:** 1. Before each `process()`, `AgentOrchestrator.compactIfNeeded()` checks token count vs threshold 2. If threshold exceeded → `compactHistory()` splits messages into compactable + recent (keep N turns) 3. Delegates summarization to `fast` tier via `orchestrator.delegate()` 4. Optionally extracts memory facts via separate delegation call 5. Replaces session history with `[summary_message, ...recent_messages]` **State Management:** - Session history: SQLite (`~/.local/share/flynn/sessions.db`) + in-memory cache in `SessionManager` - Memory: Namespace-based markdown files in `~/.local/share/flynn/memory/` - Vectors: SQLite (`~/.local/share/flynn/vectors.db`) for embeddings - Config: YAML file at `~/.config/flynn/config.yaml` (read once at startup) ## Key Abstractions **ModelClient:** - Purpose: Uniform interface to any LLM provider - Interface: `chat(request: ChatRequest): Promise` + optional `chatStream()` - Implementations: `src/models/anthropic.ts`, `src/models/openai.ts`, `src/models/gemini.ts`, `src/models/bedrock.ts`, `src/models/github.ts`, `src/models/local/ollama.ts`, `src/models/local/llamacpp.ts` - Pattern: Each provider wraps its SDK and normalizes to `ChatResponse` **ModelRouter:** - Purpose: Tier-based model selection with cascading fallback - Location: `src/models/router.ts` - Tiers: `fast`, `default`, `complex`, `local` — each maps to a `ModelClient` - Implements `ModelClient` interface itself, so consumers don't need to know about tiers **ChannelAdapter:** - Purpose: Normalize platform-specific messaging into a common interface - Interface: `connect()`, `disconnect()`, `send(peerId, msg)`, `onMessage(handler)` - Location: `src/channels/types.ts` - Pattern: Each adapter wraps a platform SDK, handles auth/filtering, emits `InboundMessage` **Tool:** - Purpose: Executable capability exposed to the AI model - Interface: `{ name, description, inputSchema, execute(args): Promise }` - Location: `src/tools/types.ts` - Registration: tool file → `src/tools/builtin/index.ts` → `src/tools/index.ts` → `src/daemon/index.ts` **Session:** - Purpose: Conversation state (message history) for a channel+sender pair - Interface: `addMessage()`, `getHistory()`, `clear()`, `replaceHistory()` - Location: `src/session/manager.ts` - ID format: `channel:senderId` (e.g., `telegram:123456`) **AgentOrchestrator:** - Purpose: Wraps NativeAgent with delegation, compaction, memory, usage tracking - Location: `src/backends/native/orchestrator.ts` - Key method: `delegate(SubAgentRequest)` — stateless single-turn call to any tier - Delegation tasks: compaction, memory extraction, classification, tool summarisation, complex reasoning **DaemonContext:** - Purpose: Holds all initialized subsystems returned by `startDaemon()` - Location: `src/daemon/index.ts` - Contains: config, lifecycle, session/model/tool/channel/gateway/mcp/skill/agent registries ## Entry Points **CLI Binary (`flynn`):** - Location: `src/cli/index.ts` - Triggers: `flynn start`, `flynn tui`, `flynn send`, `flynn sessions`, `flynn doctor`, `flynn config` - Responsibilities: Parse args, load config, bootstrap subsystems **Daemon Start:** - Location: `src/daemon/index.ts` → `startDaemon(config)` - Triggers: `flynn start` CLI command - Responsibilities: Initialize all subsystems in order, wire dependencies, start channel adapters and gateway **Gateway Server:** - Location: `src/gateway/server.ts` - Triggers: HTTP/WS connections on configured port (default 18800) - Responsibilities: JSON-RPC routing, WebSocket session management, static UI serving, webhook HTTP endpoints **TUI:** - Location: `src/frontends/tui/minimal.ts` (readline) and `src/frontends/tui/fullscreen.ts` (React/Ink) - Triggers: `flynn tui` or `flynn tui --fullscreen` - Responsibilities: Local interactive chat interface connecting to gateway via WebSocket ## Error Handling **Strategy:** Catch-and-convert with descriptive context. No global error handler. **Patterns:** - Model layer: Retry with exponential backoff → tier fallback → global fallback → throw aggregated error - Tool execution: `Promise.race` timeout → catch → return `ToolResult { success: false, error: message }` - Channel adapters: `Promise.allSettled` for start/stop — log per-adapter errors, don't crash - Daemon: Lifecycle LIFO shutdown handlers — each wrapped in try/catch - Config: Zod validation throws with structured error messages on invalid config - Gateway: JSON-RPC error codes (`ParseError`, `MethodNotFound`, `InternalError`) ## Cross-Cutting Concerns **Logging:** `console.log`/`console.error`/`console.warn`/`console.debug` throughout. No structured logging framework. Debug-level messages for model fallback decisions. **Validation:** Zod for config validation (`src/config/schema.ts`). Tool args validated by model-provided schema. No runtime validation on tool args beyond what the tool itself checks. **Authentication:** Multi-layer: - Gateway: Bearer token auth + optional Tailscale identity header (`src/gateway/auth.ts`) - Telegram: `allowed_chat_ids` whitelist - Discord: `allowed_guild_ids` + `allowed_channel_ids` whitelists - Slack: `allowed_channel_ids` whitelist + signing secret - WhatsApp: `allowed_numbers` + `allowed_group_ids` whitelists - Webhooks: HMAC signature verification (per-webhook secret) - Pairing: DM pairing codes for unknown senders (`src/channels/pairing.ts`) **Tool Policy:** Profile-based filtering (minimal/messaging/coding/full) + glob-pattern allow/deny lists + per-agent/per-provider overrides (`src/tools/policy.ts`). **Configuration:** Single YAML file with `${ENV_VAR}` expansion, validated by comprehensive Zod schema. Every subsystem is feature-toggled via config. Default config path: `~/.config/flynn/config.yaml`. --- *Architecture analysis: 2025-02-09*