___ _ _
/ __|_ _ _ _ __| |_| |_ _
| (__| '_| || (_-< _| | || |
\___|_| \_,_/__/\__|_|\_, |
|__/
🥐 Flaky & Fast
by Jeremy JEANNE
Crustly is a high-performance terminal AI assistant built in Rust, featuring:
- Multi-LLM Support: Anthropic, OpenAI, and local LLMs
- Extensible Tool System: 21 tools for file operations, code execution, agent delegation, and workflows
- Interactive TUI: Ratatui-based terminal interface with plan mode
- Local-First Storage: SQLite database for privacy and persistence
- Intelligent Prompt Analysis: Automatic tool hint detection
GitHub-renderable Mermaid versions of the diagrams below live in
docs/architecture/(C4 context/container), and an AI-queryable knowledge graph of the actual codebase lives indocs/graph/.
- System Overview
- Module Architecture
- Core Components
- Data Flow
- Tool System
- Database Layer
- Service Layer
- Configuration
- Error Handling
- Design Patterns
- Class Diagrams
┌─────────────────────────────────────────────────────────────────┐
│ USER INTERFACE │
├─────────────────────────────────────────────────────────────────┤
│ TUI (Ratatui) │ CLI (Clap) │
│ - Interactive chat │ - Single commands │
│ - Plan mode │ - Batch processing │
│ - File picker │ - Configuration │
│ - Tool approval │ - Log management │
└─────────────────────┬────────┴──────────┬───────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ AgentService │ PromptAnalyzer │
│ - Conversation management │ - Keyword detection │
│ - Tool execution loop │ - Tool hint injection │
│ - Cost tracking │ - Intent recognition │
└─────────────────────┬────────┴──────────┬───────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ PROVIDER LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Provider Trait │ Tool Registry │
│ ├─ AnthropicProvider │ ├─ ReadTool / WriteTool │
│ ├─ OpenAIProvider │ ├─ EditTool / BashTool │
│ ├─ GeminiProvider │ ├─ GlobTool / GrepTool │
│ ├─ BedrockProvider │ ├─ LsTool / WebSearchTool │
│ ├─ AzureProvider │ ├─ CodeExecTool │
│ └─ VertexAIProvider │ ├─ NotebookEditTool │
│ │ ├─ DocParserTool │
│ │ ├─ PlanTool / TaskTool │
│ │ ├─ ContextTool │
│ │ ├─ HttpClientTool │
│ │ ├─ WebFetchTool │
│ │ ├─ TodoWriteTool │
│ │ ├─ AskUserTool │
│ │ ├─ SkillTool │
│ │ ├─ AgentTool │
│ │ └─ PowerShellTool │
└─────────────────────┬────────┴──────────┬───────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
├─────────────────────────────────────────────────────────────────┤
│ SessionService │ MessageService │ PlanService │
│ - CRUD sessions │ - CRUD messages │ - Plan management │
│ - Token tracking │ - Conversation │ - Task orchestration │
│ - Cost aggregation │ - History │ - Execution workflow │
└─────────────────────┬────────┴──────────┴───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DATABASE LAYER │
├─────────────────────────────────────────────────────────────────┤
│ SQLite Database │
│ ├─ Sessions (chat history metadata) │
│ ├─ Messages (conversation content) │
│ ├─ Plans (structured task plans) │
│ ├─ PlanTasks (individual steps) │
│ ├─ Files (cached file contents) │
│ └─ ToolExecutions (audit trail) │
└─────────────────────────────────────────────────────────────────┘
| Aspect | Description |
|---|---|
| Language | Rust 2021 Edition (1.75+) |
| Async Runtime | Tokio (full features) |
| TUI Framework | Ratatui + Crossterm |
| Database | SQLite via SQLx |
| HTTP Client | Reqwest |
| Configuration | TOML-based |
| License | FSL-1.1-MIT |
src/
├── lib.rs # Library entry point & module declarations
├── main.rs # Binary entry point
├── error.rs # Global error types
│
├── app/ # Application lifecycle
│ └── mod.rs
│
├── cli/ # Command-line interface
│ └── mod.rs # CLI parsing, commands, handlers
│
├── config/ # Configuration management
│ ├── mod.rs # Config struct, loading, validation
│ ├── secrets.rs # Secret management (API keys)
│ ├── crabrace.rs # Crabrace provider registry
│ └── update.rs # Configuration updater
│
├── db/ # Database layer
│ ├── mod.rs # Database connection, pool
│ ├── models.rs # Data models (Session, Message, Plan)
│ ├── retry.rs # Retry logic for DB operations
│ └── repository/ # Repository pattern implementation
│ ├── mod.rs
│ ├── session.rs # SessionRepository
│ ├── message.rs # MessageRepository
│ ├── file.rs # FileRepository
│ └── plan.rs # PlanRepository
│
├── llm/ # LLM abstraction layer
│ ├── mod.rs
│ ├── agent/ # Agent service
│ │ ├── service.rs # AgentService (core logic)
│ │ ├── context.rs # Conversation context management
│ │ └── error.rs # Agent-specific errors
│ ├── provider/ # LLM provider abstraction
│ │ ├── trait.rs # Provider trait definition
│ │ ├── types.rs # LLM request/response types
│ │ ├── anthropic.rs # Anthropic Claude provider
│ │ ├── openai.rs # OpenAI/Local LLM provider
│ │ ├── error.rs # Provider errors
│ │ └── retry.rs # Retry logic
│ ├── tools/ # Tool system
│ │ ├── mod.rs # Tool module exports
│ │ ├── trait.rs # Tool trait definition
│ │ ├── registry.rs # ToolRegistry
│ │ ├── error.rs # Tool errors
│ │ └── [21 tool implementations...]
│ └── prompt/ # Prompt formatting
│ └── mod.rs
│
├── logging.rs # Conditional debug logging
│
├── services/ # Business logic layer
│ ├── mod.rs # ServiceContext, ServiceManager
│ ├── session.rs # SessionService
│ ├── message.rs # MessageService
│ ├── file.rs # FileService
│ └── plan.rs # PlanService
│
├── tui/ # Terminal user interface
│ ├── mod.rs
│ ├── app.rs # App state management
│ ├── runner.rs # TUI event loop
│ ├── render.rs # Rendering logic
│ ├── events.rs # Event handling
│ ├── prompt_analyzer.rs # Keyword detection & hints
│ ├── plan.rs # Plan document structure
│ ├── splash.rs # Splash screen
│ ├── highlight.rs # Syntax highlighting
│ ├── markdown.rs # Markdown rendering
│ ├── styles/ # UI styling
│ ├── components/ # Reusable UI components
│ ├── pages/ # UI pages
│ └── utils/ # TUI utilities
│
├── events/ # Global event definitions
│ └── mod.rs
│
├── message/ # Message types
│ └── mod.rs
│
├── lsp/ # Language Server Protocol
│ └── mod.rs
│
├── mcp/ # Model Context Protocol
│ └── mod.rs
│
├── sync/ # Synchronization utilities
│ └── mod.rs
│
└── utils/ # Utility functions
└── mod.rs
main.rs
└─► cli::run()
└─► tui::run() / cmd_*()
└─► App::new()
├─► AgentService
│ ├─► Provider (trait)
│ │ ├─► AnthropicProvider
│ │ └─► OpenAIProvider
│ ├─► ToolRegistry
│ │ └─► Tool (trait) x 21
│ └─► ServiceContext
│ └─► Database::pool()
├─► SessionService
├─► MessageService
├─► PlanService
└─► PromptAnalyzer
The central orchestrator for AI conversations.
pub struct AgentService {
provider: Arc<dyn Provider>, // LLM provider
context: ServiceContext, // Database access
tool_registry: Arc<ToolRegistry>, // Available tools
max_tool_iterations: usize, // Loop protection (default: 10)
default_system_prompt: Option<String>, // System prompt
auto_approve_tools: bool, // Skip approval dialogs
approval_callback: Option<ApprovalCallback>,
working_directory: PathBuf, // Tool execution directory
}Key Methods:
| Method | Purpose |
|---|---|
send_message() |
Simple message without tools |
send_message_with_tools() |
Message with tool execution |
send_message_with_tools_and_mode() |
With read-only mode support (non-streaming) |
send_message_with_tools_and_mode_streaming() |
Streaming variant — forwards text chunks via UnboundedSender<String> |
Tool Execution Loop:
┌─────────────────┐
│ User Message │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Send to LLM │
│ (with tools) │
└────────┬────────┘
│
▼
┌────────────┐
│ Tool Use? │──NO──► Return Response
└────┬───────┘
│YES
▼
┌─────────────────┐
│ Approval Check │
├─────────────────┤
│ requires_approval? │
│ auto_approve? │
│ user callback? │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Execute Tool │
│ via Registry │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Add Result to │
│ Conversation │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Back to LLM │
│ (iteration++) │
└────────┬────────┘
│
(max 10 iterations)
Unified interface for all LLM providers.
pub trait Provider: Send + Sync {
async fn complete(&self, request: LLMRequest) -> Result<LLMResponse>;
async fn stream(&self, request: LLMRequest) -> Result<ProviderStream>;
fn supports_streaming(&self) -> bool;
fn supports_tools(&self) -> bool;
fn supports_vision(&self) -> bool;
fn name(&self) -> &str;
fn default_model(&self) -> &str;
fn supported_models(&self) -> Vec<String>;
fn validate_model(&self, model: &str) -> bool;
fn context_window(&self, model: &str) -> Option<u32>;
fn calculate_cost(&self, model: &str, input: u32, output: u32) -> f64;
}Implementations:
| Provider | Key Features |
|---|---|
| AnthropicProvider | Claude models, tool use, vision |
| OpenAIProvider | GPT models, local LLMs (Ollama, LM Studio) |
| GeminiProvider | Google Gemini models |
| BedrockProvider | AWS Bedrock |
| AzureProvider | Azure OpenAI Service |
| VertexAIProvider | Google Vertex AI |
Interactive terminal interface managing user experience.
pub struct App {
// Core state
current_session: Option<Session>,
messages: Vec<DisplayMessage>,
mode: AppMode,
// Processing
is_processing: bool,
streaming_response: Option<String>, // Live text accumulator (cleared on ResponseComplete)
// Reasoning / thinking
// Populated by complete_response() from AgentResponse.thinking_text
// Rendered as collapsible [Thinking ▸/▾] block; toggled with 't'
// Plan mode
current_plan: Option<PlanDocument>,
executing_plan: bool,
// Tool approval
pending_approval: Option<ToolApprovalRequest>,
// Services
agent_service: Arc<AgentService>,
prompt_analyzer: PromptAnalyzer,
}Application Modes:
| Mode | Purpose | Key Actions |
|---|---|---|
Splash |
Startup screen | Wait 3s or press any key |
Chat |
Main conversation | Send messages, use tools |
Plan |
Plan review (read-only) | Approve/Reject/Revise plans |
Sessions |
Session management | Switch/Create sessions |
ToolApproval |
Permission dialog | Approve/Deny tool execution |
FilePicker |
File selection | Browse and select files |
Help |
Help screen | View keyboard shortcuts |
Settings |
Configuration | Modify settings |
Keyboard Shortcuts:
| Shortcut | Action |
|---|---|
Ctrl+Enter |
Submit message |
Ctrl+C |
Quit |
Ctrl+N |
New session |
Ctrl+L |
List sessions |
Ctrl+P |
Toggle plan mode |
Ctrl+A |
Approve plan (Plan mode) |
Ctrl+R |
Reject plan (Plan mode) |
Ctrl+I |
Request revision (Plan mode) |
t |
Toggle thinking panel on focused message |
@ |
Open file picker |
Esc |
Cancel/Back |
Automatically detects user intent and adds tool hints.
pub struct PromptAnalyzer {
plan_regex: Regex,
read_file_regex: Regex,
search_regex: Regex,
write_file_regex: Regex,
edit_file_regex: Regex,
bash_regex: Regex,
web_search_regex: Regex,
}Keyword Detection:
| Tool | Example Keywords |
|---|---|
| plan | "make a plan", "create a plan", "plan for" |
| read_file | "read file", "show me file", "view file" |
| grep | "search for", "find", "grep", "locate" |
| write_file | "create file", "write file", "new file" |
| edit_file | "edit file", "modify file", "update file" |
| bash | "run command", "execute command", "shell command" |
| web_search | "search online", "google", "search the web" |
Example Transformation:
Input: "make a plan for implementing JWT authentication"
Output: "make a plan for implementing JWT authentication
**TOOL HINT**: Use the `plan` tool to create a structured plan with
tasks, dependencies, and implementation steps."
User Types Message (TUI)
│
▼
┌─────────────────────┐
│ App.handle_chat_key │
│ - Collect input │
│ - Detect Ctrl+Enter │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ App.send_message() │
│ - Analyze prompt │
│ - Create chunk_tx │ ← unbounded_channel for streaming
│ - Spawn forwarder │ ← task: chunk_rx → ResponseChunk events
└─────────┬───────────┘
│
▼
┌─────────────────────────────────┐
│ AgentService │
│ .send_message_with_tools_and_ │
│ mode_streaming(chunk_tx) │
│ - Load conversation context │
│ - Build LLMRequest with tools │
│ - Call provider.stream() │
│ - drain_stream_to_response() │
│ ├─ TextDelta → route_text_delta()
│ │ ├─ outside <think> → text_buf + chunk_tx.send()
│ │ └─ inside <think> → thinking_buf (suppressed)
│ ├─ ThinkingDelta → thinking_buf (Anthropic)
│ └─ ToolUse events → pending_tool / tool_uses
└─────────┬───────────────────────┘
│
▼ (streaming)
┌─────────────────────┐
│ TUI Event Loop │
│ ResponseChunk(str) │ ← forwarded by forwarder task
│ - append_streaming_ │
│ chunk() → render │ ← live [streaming] label in UI
└─────────┬───────────┘
│ (on stream end, forwarder exits)
▼
┌─────────────────────────────────┐
│ AgentService: Tool Execution │
│ (if tool_use block present) │
│ - Approval check │
│ - Execute tool via Registry │
│ - Format result, continue loop │
└─────────┬───────────────────────┘
│
▼
┌─────────────────────┐
│ Save to Database │
│ - Message content │
│ - Token usage │
│ - Cost calculation │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ ResponseComplete │
│ (AgentResponse) │
│ - content blocks │
│ - thinking_text │ ← extracted from ContentBlock::Thinking
│ - usage / cost │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ complete_response() │
│ - clears streaming_ │
│ response │
│ - adds DisplayMsg │
│ with thinking_text│
└─────────────────────┘
│ - Message content │ │ - Token usage │ │ - Cost calculation │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ Return AgentResponse│ │ - content │ │ - usage │ │ - cost │ │ - model │ └─────────────────────┘
### 4.2 Plan Creation Flow
User: "make a plan for implementing login" │ ▼ ┌─────────────────────┐ │ AgentService │ │ - LLM recognizes │ │ plan intent │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ PlanTool.execute() │ │ operation: "create" │ │ - Create PlanDoc │ │ - Save to DB │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ PlanTool.execute() │ │ operation: "add_task"│ │ - Add task 1 │ │ - Add task 2 │ │ - Add task N │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ PlanTool.execute() │ │ operation: "finalize"│ │ - Set status to │ │ PendingApproval │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ App.check_and_load_ │ │ plan() │ │ - Load plan from DB │ │ - Show notification │ │ - Wait for user │ │ approval │ └─────────┬───────────┘ │ ┌─────┴─────┐ │ User │ │ Action │ └─────┬─────┘ │ ┌─────┼─────┬─────────┐ │ │ │ │ ▼ ▼ ▼ ▼ Ctrl+A Ctrl+R Ctrl+I Esc Approve Reject Revise Cancel │ │ │ │ ▼ ▼ ▼ ▼ Execute Clear Prefill Return Plan Plan Input to Chat
### 4.3 Tool Approval Flow
┌─────────────────────┐ │ Tool requires │ │ approval? │ └─────────┬───────────┘ │ ┌────┴────┐ │ YES │ └────┬────┘ │ ▼ ┌─────────────────────┐ │ context.auto_approve│ │ enabled? │ └─────────┬───────────┘ │ ┌────┴────┐ ┌────┐ │ NO │ │ YES│ └────┬────┘ └──┬─┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────┐ │ Create Approval │ │ Execute │ │ Request │ │ Immediately │ │ - tool_name │ └─────────────┘ │ - tool_input │ │ - capabilities │ └─────────┬───────┘ │ ▼ ┌─────────────────────┐ │ Send to TUI via │ │ mpsc channel │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ TUI: ToolApproval │ │ Mode │ │ - Display tool info │ │ - Show capabilities │ │ - 5-minute timeout │ └─────────┬───────────┘ │ ┌────┴────┐ │ User │ │ Input │ └────┬────┘ │ ┌─────┼─────┐ ▼ ▼ ▼ 'A' 'D' Timeout Approve Deny (5min) │ │ │ ▼ ▼ ▼ Execute Return Return Tool Error Error
---
## 4.4 Streaming Architecture
### Overview
Crustly streams LLM responses token-by-token to the TUI using a `tokio::sync::mpsc::unbounded_channel`:
AgentService (async task) TUI Event Loop ┌──────────────────────────┐ ┌───────────────────────┐ │ provider.stream(request) │ │ TuiEvent::ResponseChunk│ │ │ │ │ → append_streaming_ │ │ ▼ │ chunk │ chunk() │ │ drain_stream_to_response │──tx───▶│ → streaming_response │ │ │ │ │ rendered live │ │ ├─ TextDelta (visible) │ └───────────────────────┘ │ │ └─ tx.send(text) │ │ └─ ThinkingDelta / tags│ ┌───────────────────────┐ │ └─ thinking_buf │ await │ TuiEvent::ResponseComp│ │ │ fwd ──▶ lete(AgentResponse) │ │ await forwarder_handle │ │ → clear streaming_ │ │ send(ResponseComplete) │ │ response │ └──────────────────────────┘ │ → show DisplayMessage│ └───────────────────────┘
**Race-condition guarantee:** `forwarder_handle.await` is called before `ResponseComplete` is emitted. This ensures all `ResponseChunk` events are already in the TUI channel when `ResponseComplete` arrives — FIFO ordering is maintained.
### `drain_stream_to_response` (free async fn in `service.rs`)
Consumes a `ProviderStream` and assembles a complete `LLMResponse`:
| Stream event | Action |
|---|---|
| `MessageStart` | Capture response ID |
| `ContentBlockStart(ToolUse)` | Store as `pending_tool` |
| `ContentBlockStop` | Flush `pending_tool` → `tool_uses` |
| `ContentBlockDelta::TextDelta` | Route via `route_text_delta()` |
| `ContentBlockDelta::ThinkingDelta` | Append to `thinking_buf` |
| `MessageDelta` | Capture stop reason + token usage |
| `MessageStop` | Break loop |
| `StreamEvent::Error` | Return hard error immediately |
### `route_text_delta` (private fn in `service.rs`)
Statefully routes each `TextDelta` through `<think>` tag detection:
TextDelta text │ ├─ outside block ──► text_buf + chunk_tx.send() (visible to TUI) └─ inside … ► thinking_buf (hidden from TUI)
State: in_think_block: bool persists between delta calls
**Post-processing fallback:** if `thinking_buf` is empty after the stream ends (no `ThinkingDelta` events and no `<think>` tags detected in-stream), `extract_think_tags()` is run on the assembled `text_buf` as a safety net.
---
## 4.5 Reasoning / Thinking Display
### Sources
Crustly unifies three reasoning sources into a single `ContentBlock::Thinking`:
| Source | Provider | Field / Mechanism |
|--------|----------|-------------------|
| Anthropic extended thinking | AnthropicProvider | `ThinkingDelta` stream events |
| DeepSeek-R1 direct API | OpenAIProvider | `reasoning_content` JSON field |
| Ollama tag-based reasoning | OpenAIProvider | `<think>…</think>` in content text |
### `extract_think_tags(text: &str) -> (String, String)` (`types.rs`)
Utility that strips all `<think>…</think>` blocks from text:
- Returns `(thinking_content, cleaned_text)` — both trimmed
- Handles multiple blocks (joined with `\n`)
- Unclosed `<think>` tag: rest of string treated as thinking
- Case-sensitive (`<think>` only — consistent with DeepSeek/QwQ output)
### Priority logic in `from_openai_response()` (`openai.rs`)
- reasoning_content field present & non-empty? YES → use it as thinking; preserve content text verbatim (no tag stripping) NO → run extract_think_tags() on content text; tag_thinking → thinking, cleaned → visible text
### TUI rendering (`render.rs`)
DisplayMessage.thinking_text: Option │ press 't' to toggle thinking_expanded │ ┌───────┴───────┐ │ collapsed │ expanded ▼ ▼ [Thinking ▸] [Thinking ▾] ┌─────────────┐ │ thinking │ │ content │ │ rendered as │ │ markdown │ └─────────────┘
---
## 5. Tool System
### 5.1 Tool Trait
```rust
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Value; // JSON Schema
fn capabilities(&self) -> Vec<ToolCapability>;
fn requires_approval(&self) -> bool;
async fn execute(&self, input: Value, context: &ToolExecutionContext)
-> Result<ToolResult>;
fn validate_input(&self, _input: &Value) -> Result<()>;
}
pub enum ToolCapability {
ReadFiles, // Can read file contents
WriteFiles, // Can modify/create files
ExecuteShell, // Can run shell commands
Network, // Can access network
SystemModification, // Can modify system state
PlanManagement, // Can manage plans/tasks
}| # | Tool Name | File | Capabilities | Approval | Description |
|---|---|---|---|---|---|
| 1 | read_file |
read.rs | ReadFiles | No | Read file contents with line ranges |
| 2 | write_file |
write.rs | WriteFiles, SystemMod | Yes | Create or overwrite files |
| 3 | edit_file |
edit.rs | WriteFiles, SystemMod | Yes | Edit files (replace, insert, delete, regex) |
| 4 | bash |
bash.rs | ExecuteShell, SystemMod | Yes | Execute shell commands |
| 5 | ls |
ls.rs | ReadFiles | No | List directory contents |
| 6 | glob |
glob.rs | ReadFiles | No | Find files by pattern |
| 7 | grep |
grep.rs | ReadFiles | No | Search file contents (literal/regex) |
| 8 | web_search |
web_search.rs | Network | No | Internet search (DuckDuckGo) |
| 9 | execute_code |
code_exec.rs | ExecuteShell, SystemMod | Yes | Run Python/JS/Rust/Shell code |
| 10 | notebook_edit |
notebook.rs | WriteFiles, SystemMod | Yes | Edit Jupyter notebooks |
| 11 | parse_document |
doc_parser.rs | ReadFiles | No | Extract text from PDF/DOCX documents |
| 12 | task |
task.rs | PlanManagement | No | Task tracking and management |
| 13 | context |
context.rs | PlanManagement | No | Session context/variables |
| 14 | http_request |
http.rs | Network | No | Make HTTP API requests |
| 15 | plan |
plan_tool.rs | PlanManagement | No | Create and manage structured plans |
| 16 | web_fetch |
web_fetch.rs | Network | No | Fetch a URL and extract readable text |
| 17 | todo_write |
todo_write.rs | WriteFiles | No | Read/write persistent todo lists |
| 18 | ask_user |
ask_user.rs | — | No | Pause execution and ask the user a question |
| 19 | skill |
skill.rs | ReadFiles | No | Load a named skill (slash command) from SKILL.md |
| 20 | agent |
agent.rs | WriteFiles | No | Spawn a background sub-agent for a focused task |
| 21 | powershell |
powershell.rs | ExecuteShell, SystemMod, Network | Yes | Execute PowerShell (pwsh / powershell.exe) commands |
pub struct ToolExecutionContext {
pub session_id: Uuid,
pub working_directory: PathBuf,
pub env_vars: HashMap<String, String>,
pub auto_approve: bool,
pub timeout_secs: u64,
pub read_only_mode: bool, // Plan mode restriction
pub sub_agent_launcher: Option<Arc<dyn SubAgentLauncher>>, // Injected by AgentService
}SubAgentLauncher is a trait injected into the context by AgentService. AgentTool depends on it to spawn sub-agents without knowing AgentService internals. Sub-agents created via the launcher have allow_sub_agents: false to prevent infinite recursion.
Read-Only Mode Restrictions:
When read_only_mode = true:
- ❌
write_file: Blocked - ❌
edit_file: Blocked - ❌
execute_code: Blocked - ❌
notebook_edit: Blocked - ❌
agent: Blocked (cannot spawn sub-agents in plan mode) ⚠️ bash: Filters unsafe commands (>, >>, | tee, rm, mv, etc.)⚠️ powershell: Allowlist of safe cmdlets (Get-Content, Select-String, etc.); blocks redirection, Remove-Item, Invoke-Expression, etc.- ✅ All other tools: Normal operation
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self;
pub fn register(&mut self, tool: Arc<dyn Tool>);
pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>>;
pub fn has_tool(&self, name: &str) -> bool;
pub fn list_tools(&self) -> Vec<String>;
pub fn get_tool_definitions(&self) -> Vec<Tool>; // For LLM
pub async fn execute(&self, name: &str, input: Value,
context: &ToolExecutionContext) -> Result<ToolResult>;
pub fn count(&self) -> usize;
}Registration in CLI:
let mut tool_registry = ToolRegistry::new();
// Phase 1: Essential file operations
tool_registry.register(Arc::new(ReadTool));
tool_registry.register(Arc::new(WriteTool));
tool_registry.register(Arc::new(EditTool));
tool_registry.register(Arc::new(BashTool));
tool_registry.register(Arc::new(LsTool));
tool_registry.register(Arc::new(GlobTool));
tool_registry.register(Arc::new(GrepTool));
// Phase 2: Advanced features
tool_registry.register(Arc::new(WebSearchTool));
tool_registry.register(Arc::new(CodeExecTool));
tool_registry.register(Arc::new(NotebookEditTool));
tool_registry.register(Arc::new(DocParserTool));
// Phase 3: Workflow & integration
tool_registry.register(Arc::new(TaskTool));
tool_registry.register(Arc::new(ContextTool));
tool_registry.register(Arc::new(HttpClientTool));
tool_registry.register(Arc::new(PlanTool));
// Phase 4: Claw Code parity
tool_registry.register(Arc::new(WebFetchTool));
tool_registry.register(Arc::new(TodoWriteTool));
tool_registry.register(Arc::new(AskUserTool));
tool_registry.register(Arc::new(SkillTool));
tool_registry.register(Arc::new(AgentTool));
tool_registry.register(Arc::new(PowerShellTool));pub struct Database {
pool: SqlitePool,
}
impl Database {
pub async fn connect<P: AsRef<Path>>(path: P) -> Result<Self>;
pub async fn connect_in_memory() -> Result<Self>;
pub fn pool(&self) -> &SqlitePool;
pub fn is_connected(&self) -> bool;
pub async fn run_migrations(&self) -> Result<()>;
pub async fn close(self) -> Result<()>;
}Connection Configuration:
- Max connections: 5
- Busy timeout: 5 seconds
- WAL mode for concurrency
- SQLx migrations
Session:
pub struct Session {
pub id: Uuid,
pub title: Option<String>,
pub model: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub archived_at: Option<DateTime<Utc>>,
pub token_count: i32,
pub total_cost: f64,
}Message:
pub struct Message {
pub id: Uuid,
pub session_id: Uuid,
pub role: String,
pub content: String,
pub sequence: i32,
pub created_at: DateTime<Utc>,
pub token_count: Option<i32>,
pub cost: Option<f64>,
}Plan:
pub struct Plan {
pub id: Uuid,
pub session_id: Uuid,
pub title: String,
pub description: String,
pub context: String,
pub risks: String, // JSON array
pub test_strategy: String,
pub technical_stack: String, // JSON array
pub status: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub approved_at: Option<DateTime<Utc>>,
}PlanTask:
pub struct PlanTask {
pub id: Uuid,
pub plan_id: Uuid,
pub task_order: i32,
pub title: String,
pub description: String,
pub task_type: String,
pub dependencies: String, // JSON array
pub complexity: i32,
pub acceptance_criteria: String,
pub status: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}Service Layer
│
▼
Repository (trait)
│
├─► SessionRepository
│ - create(session)
│ - find_by_id(id)
│ - list(options)
│ - update(session)
│ - delete(id)
│
├─► MessageRepository
│ - create(message)
│ - find_by_session(session_id)
│ - find_by_id(id)
│ - update(message)
│ - delete_by_session(session_id)
│
└─► PlanRepository
- create(plan)
- find_by_id(id)
- find_by_session(session_id)
- update(plan)
- delete(id)
Shared dependency injection container.
pub struct ServiceContext {
pub pool: Arc<Pool>,
}
impl ServiceContext {
pub fn new(pool: Arc<Pool>) -> Self;
pub fn clone(&self) -> Self;
}SessionService:
- Session CRUD operations
- Token and cost tracking
- Archive management
MessageService:
- Message CRUD operations
- Conversation history management
- Usage tracking per message
PlanService:
- Plan lifecycle management
- JSON import/export
- Status transitions
FileService:
- Cached file operations
- Content versioning
- File metadata
// Example: SessionService
pub struct SessionService {
context: ServiceContext,
}
impl SessionService {
pub fn new(context: ServiceContext) -> Self {
Self { context }
}
pub async fn create_session(&self, title: Option<String>) -> Result<Session> {
let session = Session {
id: Uuid::new_v4(),
title,
model: None,
created_at: Utc::now(),
updated_at: Utc::now(),
archived_at: None,
token_count: 0,
total_cost: 0.0,
};
SessionRepository::create(self.context.pool(), &session).await?;
Ok(session)
}
// ... other methods
}pub struct Config {
pub crabrace: CrabraceConfig,
pub database: DatabaseConfig,
pub logging: LoggingConfig,
pub debug: DebugConfig,
pub providers: ProviderConfigs,
}
pub struct ProviderConfigs {
pub anthropic: Option<ProviderConfig>,
pub openai: Option<ProviderConfig>,
pub gemini: Option<ProviderConfig>,
pub bedrock: Option<ProviderConfig>,
pub azure: Option<ProviderConfig>,
pub vertex: Option<ProviderConfig>,
}
pub struct ProviderConfig {
pub enabled: bool,
pub api_key: Option<String>,
pub base_url: Option<String>,
pub default_model: Option<String>,
}
pub struct DatabaseConfig {
pub path: PathBuf,
pub max_connections: u32,
pub busy_timeout_secs: u64,
}- Default configuration
- Config file (
~/.config/crustly/config.toml) - Environment variables
- CLI arguments
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic Claude API key |
OPENAI_API_KEY |
OpenAI API key |
OPENAI_BASE_URL |
Custom OpenAI-compatible endpoint |
CRUSTLY_CONFIG |
Custom config file path |
RUST_LOG |
Log level filter |
pub enum CrustlyError {
Database(sqlx::Error),
Io(std::io::Error),
Config { message: String, code: ErrorCode },
Provider { provider: String, message: String, code: ErrorCode },
ToolExecution { tool: String, message: String, code: ErrorCode },
PermissionDenied(String),
}
pub enum ErrorCode {
// Configuration (1000-1999)
ConfigNotFound = 1000,
ConfigInvalid = 1001,
ConfigMergeError = 1002,
// Provider (2000-2999)
ProviderNotFound = 2000,
ProviderAuthFailed = 2001,
ProviderRateLimit = 2002,
ProviderTimeout = 2003,
// Tool (3000-3999)
ToolNotFound = 3000,
ToolExecutionFailed = 3001,
ToolTimeout = 3002,
// Permission (4000-4999)
PermissionDenied = 4000,
PermissionNotGranted = 4001,
}pub enum ToolError {
NotFound(String),
InvalidInput(String),
Execution(String),
ApprovalRequired(String),
Io(io::Error),
Timeout,
}Tool → ToolError
│
▼
ToolRegistry → Result<ToolResult>
│
▼
AgentService → AgentError
│
▼
App → TuiEvent::Error(String)
│
▼
User sees error message in UI
- Provider Trait: Unified LLM interface
- Tool Trait: Extensible tool system
- Repository Pattern: Database abstraction
// AgentService configuration
AgentService::new(provider, context)
.with_system_prompt("...")
.with_tool_registry(registry)
.with_auto_approve_tools(false)
.with_approval_callback(Some(callback))
.with_max_tool_iterations(10)
.with_working_directory(dir)
// ToolExecutionContext
ToolExecutionContext::new(session_id)
.with_auto_approve(false)
.with_timeout(30)
.with_read_only_mode(false)
.with_working_directory(dir)
// LogConfig
LogConfig::new()
.with_debug_mode(true)
.with_log_level(Level::DEBUG)
.with_log_dir(path)- ToolRegistry: Dynamic tool management
- Runtime registration and lookup
- Tool definition generation for LLM
- ServiceContext: Dependency injection
- ServiceManager: Facade
- Individual services (Session, Message, Plan, File)
pub enum TuiEvent {
Key(KeyEvent),
MessageSubmitted(String),
ResponseChunk(String), // streaming: partial text token
ResponseComplete(AgentResponse),
ToolApprovalRequested(ToolApprovalRequest),
ToolApprovalResponse(ToolApprovalResponse),
// ...
}
// Event loop
loop {
match app.next_event().await {
Some(event) => app.handle_event(event).await?,
None => break,
}
}provider: Arc<dyn Provider>,
tool_registry: Arc<ToolRegistry>,
agent_service: Arc<AgentService>,
pool: Arc<SqlitePool>,See docs/architecture.puml for the complete PlantUML class diagram.
To render:
# Install PlantUML
brew install plantuml # macOS
apt install plantuml # Linux
# Generate diagram
plantuml docs/architecture.puml
# Or use online renderer
# https://www.plantuml.com/plantuml/┌─────────────────┐ ┌─────────────────┐
│ App │ │ AgentService │
├─────────────────┤ uses ├─────────────────┤
│ agent_service │────────▶│ provider │
│ prompt_analyzer │ │ tool_registry │
│ session_service │ │ context │
│ message_service │ └─────────────────┘
│ plan_service │ │
└─────────────────┘ │uses
▼
┌─────────────────┐
│ ToolRegistry │
├─────────────────┤
│ tools: HashMap │
│ register() │
│ execute() │
└─────────────────┘
│manages
▼
┌─────────────────┐
│ Tool (trait) │
├─────────────────┤
│ name() │
│ execute() │
│ capabilities() │
└─────────────────┘
△
┌───────────────┼───────────────┐
│ │ │
┌───────┴───────┐ ┌─────┴─────┐ ┌──────┴──────┐
│ ReadTool │ │ WriteTool │ │ ... │
└───────────────┘ └───────────┘ └─────────────┘
┌─────────────────────┐
│ Provider (trait) │
├─────────────────────┤
│ +complete() │
│ +stream() │
│ +calculate_cost() │
│ +context_window() │
└─────────────────────┘
△
│implements
┌─────┴─────┐
│ │
┌───┴───┐ ┌───┴───┐
│Anthropic│ │OpenAI │
│Provider │ │Provider│
└─────────┘ └─────────┘
┌─────────────────┐
│ ServiceContext │
├─────────────────┤
│ pool: Arc<Pool> │
└─────────────────┘
│
wraps│
▼
┌─────────────────┐
│ Database │
├─────────────────┤
│ pool: SqlitePool│
│ connect() │
│ run_migrations()│
└─────────────────┘
│
▼
┌─────────────────┐ operates on ┌─────────────┐
│ Repository │──────────────────▶│ Model │
├─────────────────┤ ├─────────────┤
│ create() │ │ Session │
│ find_by_id() │ │ Message │
│ update() │ │ Plan │
│ delete() │ │ PlanTask │
└─────────────────┘ └─────────────┘
Total files: ~150+
Total lines of code: ~25,000+
Primary language: Rust (100%)
Dependencies: 652 crates
| Operation | Expected Performance |
|---|---|
| Tool execution | <100ms (local tools) |
| LLM request | 1-30s (network dependent) |
| Database query | <10ms |
| TUI rendering | 60 FPS |
| Build time (debug) | ~90s |
| Binary size | ~50MB |
- Tool Approval System: Dangerous operations require user consent
- Read-Only Mode: Plan mode restricts write operations
- API Key Management: Secure storage via environment variables
- Input Validation: All tool inputs validated before execution
- Command Filtering: Bash tool filters unsafe commands in read-only mode
- RAG (Retrieval-Augmented Generation) support
- Vector store integration
- More LLM providers (Gemini, Azure, Vertex)
- Plugin system for custom tools
- Web interface
- Multi-user support
- Enhanced LSP integration (currently a stub)
-
Real-time streaming TUI(implemented) -
Reasoning / thinking display(implemented — DeepSeek-R1, QwQ-32B, Anthropic)
Document Version: 1.2 Last Updated: May 2026 Author: Jeremy JEANNE License: FSL-1.1-MIT