13 KiB
Owlen Architecture
This document provides a high-level overview of the Owlen architecture. Its purpose is to help developers understand how the different parts of the application fit together.
Core Concepts
The architecture is designed to be modular and extensible, centered around a few key concepts:
- Provider Manager: Coordinates multiple
ModelProviderimplementations, aggregates model metadata, and caches health status for the UI. - Providers: Concrete backends (Ollama Local, Ollama Cloud, future providers) accessed either directly or through MCP servers.
- Session: Manages the conversation history and state.
- TUI: The terminal user interface, built with
ratatui. - Events: A system for handling user input and other events.
Component Interaction
A simplified diagram of how components interact:
[User Input] -> [Event Loop] -> [Message Handler] -> [Session Controller] -> [Provider Manager] -> [Provider]
^ |
| v
[TUI Renderer] <- [AppMessage Stream] <- [Background Worker] <--------------- [Provider Health]
- User Input: The user interacts with the TUI, generating events (e.g., key presses).
- Event Loop: The non-blocking event loop in
owlen-tuibundles raw input, async session events, and background health updates intoAppMessageevents. - Message Handler:
App::handle_messagecentralises dispatch, updating runtime state (chat, model picker, provider indicators) before the UI redraw. - Session Controller: Prompt events create
GenerateRequests that flow throughProviderManager::generateto the designated provider. - Provider: The provider formats requests for its API and streams back
GenerateChunks. - Provider Manager: Tracks health while streaming; errors mark a provider unavailable so background workers and the model picker reflect the state.
- Background Worker: A periodic task runs health checks and emits status updates as
AppMessage::ProviderStatusevents. - TUI Renderer: The response is processed, the session state is updated, and the TUI is re-rendered to display the new information.
Crate Breakdown
owlen-core: Defines theLlmProviderabstraction, routing, configuration, session state, encryption, and the MCP client layer. This crate is UI-agnostic and must not depend on concrete providers, terminals, or blocking I/O.owlen-tui: Hosts all terminal UI behaviour (event loop, rendering, input modes) while delegating business logic and provider access back toowlen-core.owlen-cli: Small entry point that parses command-line options, resolves configuration, selects providers, and launches either the TUI or headless agent flows by calling intoowlen-core.owlen-mcp-llm-server: Runs concrete providers (e.g., Ollama Local, Ollama Cloud) behind an MCP boundary, exposing them asgenerate_texttools. This crate owns provider-specific wiring and process sandboxing.owlen-mcp-server: Generic MCP server for file operations, resource projection, and other non-LLM tools.owlen-providers: Houses concrete provider adapters (today: Ollama local + cloud) that the MCP servers embed.
Boundary Guidelines
- owlen-core: The dependency ceiling for most crates. Keep it free of terminal logic, CLIs, or provider-specific HTTP clients. New features should expose traits or data types here and let other crates supply concrete implementations.
- owlen-cli: Only orchestrates startup/shutdown. Avoid adding business logic; when a new command needs behaviour, implement it in
owlen-coreor another library crate and invoke it from the CLI. - owlen-mcp-llm-server: The only crate that should directly talk to Ollama (or other provider processes). TUI/CLI code communicates with providers exclusively through MCP clients in
owlen-core.
Provider Boundaries & MCP Topology
Owlen’s runtime is intentionally layered so that user interfaces never couple to provider-specific code. The flow can be visualised as:
[owlen-tui] / [owlen-cli]
│
│ chat + model requests
▼
[owlen-core::ProviderManager] ──> Arc<dyn ModelProvider>
│ ▲
│ │ implements `ModelProvider`
▼ │
[owlen-core::mcp::RemoteMcpClient] ─────┘
│ (JSON-RPC over stdio)
▼
┌───────────────────────────────────────────────────────────┐
│ MCP Process Boundary (spawned per provider) │
│ │
│ crates/mcp/llm-server ──> owlen-providers::ollama::* │
│ crates/mcp/server ──> filesystem & workspace tools │
│ crates/mcp/prompt-server ─> template rendering helpers │
└───────────────────────────────────────────────────────────┘
- ProviderManager (owlen-core) keeps the registry of
ModelProviderimplementations, merges model catalogues, and caches health. Local Ollama and Cloud Ollama appear as separate providers whose metadata is merged for the UI. - RemoteMcpClient (owlen-core) is the default
ModelProvider. It implements both the MCP client traits and theModelProviderinterface, allowing it to bridge chat streams back into the ProviderManager without exposing transport details. - MCP servers (crates/mcp/*) are short-lived binaries with narrowly scoped responsibilities:
crates/mcp/llm-serverwrapsowlen-providers::ollamabackends and exposesgenerate_text/list_models.crates/mcp/serveroffers tool calls (file reads/writes, search).crates/mcp/prompt-serverrenders prompt templates.
- owlen-providers contains the actual provider adapters (Ollama local & cloud today). MCP servers embed these adapters directly; nothing else should reach into them.
Health & Model Discovery Flow
- Frontends call
ProviderManager::list_all_models(). The manager fans out health checks to each registered provider (including the MCP client) and collates their models into a single list tagged with scope (Local,Cloud, etc.). - The TUI model picker (
owlen-tui/src/widgets/model_picker.rs) reads those annotated entries to drive filters like Local, Cloud, and Available. - When the user kicks off a chat, the TUI emits a request that flows through
Session::send_message, which delegates toProviderManager::generate. The selected provider (usuallyRemoteMcpClient) streams chunks back across the MCP transport and the manager updates health status based on success or failure. - Tool invocations travel the same transport: the MCP client sends tool calls to
crates/mcp/server, and responses surface as consent prompts or streamed completions in the UI.
MCP Architecture (Phase 10)
As of Phase 10, OWLEN uses a MCP-only architecture where all LLM interactions go through the Model Context Protocol:
[TUI/CLI] -> [RemoteMcpClient] -> [MCP LLM Server] -> [Ollama Provider] -> [Ollama API]
Benefits of MCP Architecture
- Separation of Concerns: The TUI/CLI never directly instantiates provider implementations.
- Process Isolation: LLM interactions run in a separate process, improving stability.
- Extensibility: New providers can be added by implementing MCP servers.
- Multi-Transport: Supports STDIO, HTTP, and WebSocket transports.
- Tool Integration: MCP servers can expose tools (file operations, web search, etc.) to the LLM.
MCP Communication Flow
- Client Creation:
RemoteMcpClient::new()spawns an MCP server binary via STDIO. - Initialization: Client sends
initializerequest to establish protocol version. - Tool Discovery: Client calls
tools/listto discover available LLM operations. - Chat Requests: Client calls the
generate_texttool with chat parameters. - Streaming: Server sends progress notifications during generation, then final response.
- Response Handling: Client skips notifications and returns the final text to the caller.
Cloud Provider Support
For Ollama Cloud providers, the MCP server accepts an OLLAMA_URL environment variable:
let env_vars = HashMap::from([
("OLLAMA_URL".to_string(), "https://cloud-provider-url".to_string())
]);
let config = McpServerConfig {
command: "path/to/owlen-mcp-llm-server",
env: env_vars,
transport: "stdio",
...
};
let client = RemoteMcpClient::new_with_config(&config)?;
Vim Mode State Machine
The TUI follows a Vim-inspired modal workflow. Maintaining the transitions keeps keyboard handling predictable:
- Normal → Insert: triggered by keys such as
i,a, oro; pressingEscreturns to Normal. - Normal → Visual:
venters visual selection;Escor completing a selection returns to Normal. - Normal → Command:
:opens command mode; executing a command or cancelling withEscreturns to Normal. - Normal → Auxiliary modes:
?(help),:provider,:model, and similar commands open transient overlays that always exit back to Normal once dismissed. - Insert/Visual/Command → Normal: pressing
Escalways restores the neutral state.
The status line shows the active mode (for example, “Normal mode • Press F1 for help”), which doubles as a quick regression check during manual testing.
Session Management
The session management system is responsible for tracking the state of a conversation. The two main structs are:
Conversation: Found inowlen-core, this struct holds the messages of a single conversation, the model being used, and other metadata. It is a simple data container.SessionController: This is the high-level controller that manages the active conversation. It handles:- Storing and retrieving conversation history via the
ConversationManager. - Managing the context that is sent to the LLM provider.
- Switching between different models by selecting a provider ID managed by
ProviderManager. - Sending requests to the provider and handling the responses (both streaming and complete).
- Storing and retrieving conversation history via the
When a user sends a message, the SessionController adds the message to the current Conversation, sends the updated message list to the Provider, and then adds the provider's response to the Conversation.
Event Flow
The event flow is managed by the EventHandler in owlen-tui. It operates in a loop, waiting for events and dispatching them to the active application (ChatApp or CodeApp).
- Event Source: Events are primarily generated by
crosstermfrom user keyboard input. Asynchronous events, like responses from aProvider, are also fed into the event system via atokio::mpscchannel. EventHandler::next(): The main application loop calls this method to wait for the next event.- Event Enum: Events are defined in the
owlen_tui::events::Eventenum. This includesKeyevents,Tickevents (for UI updates), andMessageevents (for async provider data). - Dispatch: The application's
runmethod matches on theEventtype and calls the appropriate handler function (e.g.,dispatch_key_event). - State Update: The handler function updates the application state based on the event. For example, a key press might change the
InputModeor modify the text in the input buffer. - Re-render: After the state is updated, the UI is re-rendered to reflect the changes.
TUI Rendering Pipeline
The TUI is rendered on each iteration of the main application loop in owlen-tui. The process is as follows:
tui.draw(): The main loop calls this method, passing the current application state.Terminal::draw(): This method, fromratatui, takes a closure that receives aFrame.- UI Composition: Inside the closure, the UI is built by composing
ratatuiwidgets. The root UI is defined inowlen_tui::ui::render, which builds the main layout and calls other functions to render specific components (like the chat panel, input box, etc.). - State-Driven Rendering: Each rendering function takes the current application state as an argument. It uses this state to decide what and how to render. For example, the border color of a panel might change if it is focused.
- Buffer and Diff:
ratatuidoes not draw directly to the terminal. Instead, it renders the widgets to an in-memory buffer. It then compares this buffer to the previous buffer and only sends the necessary changes to the terminal. This is highly efficient and prevents flickering.
The command palette and other modal helpers expose lightweight state structs in owlen_tui::state. These components keep business logic (suggestion filtering, selection state, etc.) independent from rendering, which in turn makes them straightforward to unit test.