Skip to content

Latest commit

 

History

History
1601 lines (1292 loc) · 48.9 KB

File metadata and controls

1601 lines (1292 loc) · 48.9 KB

Project: quickcode (Rust Reimplementation Spec)

Derived from full analysis of the quickcode TypeScript codebase. Target: A Rust implementation with identical behavior, named quickcode.


Table of Contents

  1. Overview
  2. Architecture
  3. Core Modules
  4. Data Models
  5. APIs & Interfaces
  6. Execution Flow
  7. Business Logic
  8. External Dependencies
  9. Configuration
  10. Error Handling
  11. Edge Cases
  12. Testing Requirements

1. Overview

quickcode is an AI-powered coding agent platform. It is a CLI tool that acts as an AI assistant for software development. Users run it in their project directory; it opens a Terminal UI (TUI), connects to an LLM provider, and can autonomously read, write, and edit files, execute shell commands, search code, and interact with language servers.

Key Capabilities

  • Multi-turn AI conversations (sessions) with full message history
  • File system tools: read, write, edit, glob search, grep search
  • Shell execution via a bash tool
  • Language Server Protocol (LSP) integration for code intelligence
  • Web fetch and web search tools
  • Model Context Protocol (MCP) support (tool exposure + consumption)
  • Permission system with allow/deny/ask rules for sensitive operations
  • Multiple LLM providers (Anthropic, OpenAI, Google, Azure, Bedrock, etc.)
  • HTTP API server (headless mode) with WebSocket event streaming
  • Plugin system for custom tools, agents, auth methods
  • Snapshot-based change tracking and revert capability
  • Session compaction (summarization) when context overflows
  • Sub-agent / task delegation
  • TUI built with ratatui (Rust equivalent of OpenTUI/SolidJS TUI)

Deployment Modes

  1. TUI mode (default): interactive terminal application
  2. Serve mode: headless HTTP API server, clients connect via SDK or browser
  3. Web mode: proxy to web app, serves browser-based UI

2. Architecture

2.1 High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│                    CLI Entry Point                       │
│              (yargs-equivalent in Rust: clap)           │
└────────┬────────────────────────┬───────────────────────┘
         │                        │
    ┌────▼────┐              ┌────▼────┐
    │   TUI   │              │ Server  │
    │ (tui)   │              │ (axum)  │
    └────┬────┘              └────┬────┘
         │                        │
    ┌────▼────────────────────────▼────┐
    │           Core Services          │
    │  Session | Agent | Tool | LSP    │
    │  Permission | Config | Provider  │
    └────────────────┬─────────────────┘
                     │
    ┌────────────────▼─────────────────┐
    │           Storage Layer          │
    │        SQLite (via sqlx)         │
    └──────────────────────────────────┘

2.2 Component Responsibilities

Component Responsibility
CLI Parse args, dispatch to TUI or Server mode
Server HTTP API (axum), WebSocket events, route handlers
TUI Interactive terminal UI using ratatui
Session Conversation lifecycle, message CRUD, compaction
Agent Agent configuration, LLM generation, tool dispatch
Tool Registry Dynamic tool resolution, execution, permission checking
Provider LLM provider abstraction, model discovery, auth
Permission Rule-based allow/deny/ask for file and tool access
LSP Language server clients per language
Config JSONC config loading, merging, env substitution
Snapshot Git-based file change tracking
Bus In-process pub/sub for events across components
MCP Model Context Protocol server + client
Plugin Dynamic plugin loading, hook dispatch

2.3 Instance Model

Each project directory maps to a single Instance — a context-local singleton that holds the project state, active LSP servers, database connection, and all running state. Multiple directories can be open simultaneously as separate instances.

The Instance is identified by its directory (canonicalized absolute path). Its worktree is the git worktree root (for git projects) or / (for non-git projects). All path permission checks use the instance's directory and worktree as boundaries.

2.4 Event Bus

A typed pub/sub bus (Bus) enables loose coupling between components. Events are defined with a type string and a zod/serde-compatible schema. Components publish events; subscribers receive them. There is also a GlobalBus (simple EventEmitter) for cross-instance events.

Key events:

  • session.created, session.updated, session.deleted
  • session.diff (file changes)
  • session.error
  • server.connected, global.disposed
  • server.instance.disposed

3. Core Modules

3.1 session

Manages conversation sessions. A session belongs to a project and contains an ordered list of messages. Each message has parts (text, tool calls, tool results, etc.).

Key operations:

  • create(project_id, title, directory, agent)Session
  • fork(session_id)Session (copies all messages)
  • archive(session_id) — soft-delete, sets time_archived
  • send_message(session_id, user_input, model, agent) → async stream
  • compact(session_id) — run compaction to reduce token count
  • share(session_id) → URL
  • revert(session_id, message_id) — restore filesystem to snapshot at that point
  • list(project_id)Vec<Session>

3.2 agent

Defines the agents available for use. Built-in agents:

Agent Mode Description
build primary Full tool access, file editing, command execution
plan primary Read-only, no file edits, asks before bash
general subagent General multi-step tasks, spawned as subtask
explore subagent Fast codebase exploration
compaction subagent Summarizes old messages for compaction
title subagent Generates session titles
summary subagent Generates session diffs/summaries

Each agent has:

  • name: string identifier
  • description: displayed in UI
  • mode: "primary" | "subagent" | "all"
  • permission: optional permission ruleset override
  • model: optional model override (provider/model string)
  • temperature, top_p: optional model params
  • native: bool (built-in vs user-defined)
  • hidden: bool (not shown in UI)

3.3 tool

Each tool is defined with:

  • name: string identifier (used in LLM tool calls)
  • description: shown to the LLM
  • parameters: JSON Schema (serde-compatible struct)
  • execute(args, ctx)ToolOutput

Tool output:

struct ToolOutput {
    output: String,           // text result shown to LLM
    title: Option<String>,    // short title for UI
    metadata: Option<Value>,  // structured metadata
    attachments: Vec<Attachment>, // binary files (images, PDFs)
}

struct Attachment {
    media_type: String,   // MIME type
    data: Vec<u8>,        // base64-decoded bytes
    filename: Option<String>,
}

Tool context (ctx):

struct ToolContext {
    session_id: SessionId,
    message_id: MessageId,
    call_id: String,
    abort: AbortHandle,
    // permission checking, file operations, etc.
}

3.4 provider

Abstracts LLM providers. Each provider:

  • Has an ID (e.g., anthropic, openai, google)
  • Exposes available models
  • Provides auth mechanisms (API key, OAuth, env vars)
  • Wraps the underlying API client

Models are fetched from https://models.dev/api.json and cached locally. On startup the cache is refreshed (non-blocking). A bundled snapshot exists as a fallback.

3.5 permission

Rule-based permission system. Rules are evaluated last-match-wins from a list:

struct Rule {
    permission: String,   // tool name or permission type (supports wildcards)
    pattern: String,      // file path pattern (supports wildcards)
    action: Action,       // "allow" | "deny" | "ask"
}

enum Action { Allow, Deny, Ask }

Permission evaluation:

  1. Merge rulesets (global config → session config → agent config)
  2. Find last matching rule (both permission AND pattern must match via wildcard)
  3. If no match: default to ask

When action is ask: create a PermissionRequest, send to UI/client, await response. If user denies, throw PermissionRejectedError.

Known permission types:

  • bash — shell command execution
  • external_directory — file access outside project dir
  • lsp — LSP operations (always allow with wildcard pattern)
  • doom_loop — repeated identical tool calls (loop detection)
  • plan_exit — exiting plan mode

3.6 lsp

Manages Language Server Protocol clients. One LSP server process per language per instance. The lsp tool exposes LSP operations to the AI agent.

Supported operations:

  • go_to_definition(file, line, col)
  • find_references(file, line, col)
  • hover(file, line, col)
  • document_symbol(file)
  • workspace_symbol(query)
  • go_to_implementation(file, line, col)
  • prepare_call_hierarchy(file, line, col)
  • incoming_calls(file, line, col)
  • outgoing_calls(file, line, col)

All positions are 1-based in user input, converted to 0-based for LSP protocol.

3.7 config

Loads configuration from JSONC files with:

  • Environment variable substitution: {env:VAR_NAME}
  • File content substitution: {file:path/to/file}
  • Comment support (JSONC)
  • Trailing comma support

Config files loaded in order (later overrides earlier):

  1. ~/.config/quickcode/config.jsonc (global user config)
  2. .quickcode/quickcode.jsonc files walking up from project dir to worktree root
  3. ~/.quickcode/quickcode.jsonc
  4. quickcode_CONFIG_DIR env var if set

3.8 snapshot

Tracks file changes using git. At the start of each "step" (AI response turn), a snapshot is taken (git stash-like state). At the end of the step, a patch is computed showing what changed. This enables per-message diffs and the revert operation.

The Snapshot.FileDiff type:

struct FileDiff {
    filename: String,
    patch: String,    // unified diff format
}

3.9 bus

In-process typed pub/sub. Events are typed by a string type + payload schema. Subscriptions can be callback-based or stream-based.

3.10 mcp

Model Context Protocol support:

  • Server side: Expose quickcode tools as MCP resources/tools
  • Client side: Consume MCP servers defined in config, exposing their tools to the agent

3.11 storage

SQLite database using sqlx (Rust). Tables:

  • project
  • session
  • message
  • part
  • todo
  • permission

Database location: ~/.local/share/quickcode/quickcode.db (or quickcode_HOME override).


4. Data Models

4.1 Project

struct Project {
    id: ProjectId,          // derived from directory path, URL-slug format
    worktree: String,       // git root or project dir
    vcs: Option<String>,    // "git" or None
    name: Option<String>,
    icon_url: Option<String>,
    icon_color: Option<String>,
    time_created: i64,      // Unix milliseconds
    time_updated: i64,
    time_initialized: Option<i64>,
    sandboxes: Vec<String>, // additional sandbox paths
    commands: Option<ProjectCommands>,
}

struct ProjectCommands {
    start: Option<String>,
}

ProjectId generation: SHA-256 hash of the canonical directory path, truncated to 8 chars, prefixed with p_.

4.2 Session

struct Session {
    id: SessionId,               // ascending ULID with "s_" prefix
    project_id: ProjectId,
    workspace_id: Option<WorkspaceId>,  // multi-tenant
    parent_id: Option<SessionId>,       // for forked sessions
    slug: String,                // human-readable identifier
    directory: String,           // working directory for this session
    title: String,
    version: String,             // quickcode version when created
    share_url: Option<String>,
    summary: Option<SessionSummary>,
    revert: Option<RevertInfo>,
    permission: Option<Vec<Rule>>,
    time_created: i64,
    time_updated: i64,
    time_compacting: Option<i64>,
    time_archived: Option<i64>,
}

struct SessionSummary {
    additions: i32,
    deletions: i32,
    files: i32,
    diffs: Vec<FileDiff>,
}

struct RevertInfo {
    message_id: MessageId,
    part_id: Option<PartId>,
    snapshot: Option<String>,
    diff: Option<String>,
}

4.3 Message (V2)

enum Message {
    User(UserMessage),
    Assistant(AssistantMessage),
}

struct UserMessage {
    id: MessageId,
    session_id: SessionId,
    role: "user",
    parts: Vec<Part>,
    time: MessageTime,
}

struct AssistantMessage {
    id: MessageId,
    session_id: SessionId,
    parent_id: Option<MessageId>,  // ID of the user message this replies to
    role: "assistant",
    agent: String,                 // agent name
    model_id: String,
    provider_id: String,
    system: Vec<String>,          // system prompts used
    parts: Vec<Part>,
    finish: Option<FinishReason>,
    error: Option<MessageError>,
    summary: Option<String>,      // set if this message is a compaction summary
    cost: f64,
    tokens: TokenUsage,
    time: AssistantMessageTime,
}

struct AssistantMessageTime {
    created: i64,
    completed: Option<i64>,
}

struct TokenUsage {
    input: i64,
    output: i64,
    cache_read: Option<i64>,
    cache_write: Option<i64>,
    reasoning: Option<i64>,
}

4.4 Message Parts

Parts are the atomic units within a message:

enum Part {
    Text(TextPart),
    Tool(ToolPart),
    File(FilePart),
    Snapshot(SnapshotPart),
    Patch(PatchPart),
    Reasoning(ReasoningPart),
    Subtask(SubtaskPart),
    Retry(RetryPart),
    StepStart(StepStartPart),
    StepFinish(StepFinishPart),
    Compaction(CompactionPart),
    Agent(AgentPart),
}

// Common fields on all parts:
// id: PartId, message_id: MessageId, session_id: SessionId

struct TextPart {
    text: String,
    time: StartEndTime,
    metadata: Option<Value>,  // provider-specific metadata
}

struct ReasoningPart {
    text: String,
    time: StartEndTime,
    metadata: Option<Value>,
}

struct ToolPart {
    tool: String,       // tool name
    call_id: String,    // LLM-assigned call ID
    state: ToolState,
    metadata: Option<Value>,
}

enum ToolState {
    Pending {
        input: Value,
        raw: String,
    },
    Running {
        input: Value,
        time: StartTime,
    },
    Completed {
        input: Value,
        output: String,
        title: Option<String>,
        metadata: Option<Value>,
        attachments: Vec<Attachment>,
        time: StartEndTime,
    },
    Error {
        input: Value,
        error: String,
        time: StartEndTime,
    },
}

struct FilePart {
    url: String,         // file URL or data URI
    media_type: String,
    filename: Option<String>,
}

struct SnapshotPart {
    snapshot: String,    // git stash-like identifier
}

struct PatchPart {
    hash: String,
    files: Vec<FileDiff>,
}

struct StepStartPart {
    snapshot: Option<String>,
}

struct StepFinishPart {
    reason: FinishReason,
    snapshot: Option<String>,
    tokens: TokenUsage,
    cost: f64,
}

struct CompactionPart {
    summary: String,
}

struct SubtaskPart {
    session_id: SessionId,
    agent: String,
}

struct AgentPart {
    agent: String,
}

struct RetryPart {
    error: MessageError,
    attempt: i32,
    delay_ms: i64,
}

struct StartEndTime { start: i64, end: i64 }
struct StartTime { start: i64 }

4.5 Message Errors

enum MessageError {
    AuthError { provider_id: String, message: String },
    APIError { provider_id: String, message: String, status_code: Option<u16>, request_id: Option<String>, data: Option<Value> },
    ContextOverflowError { provider_id: String, message: String },
    OutputLengthError { provider_id: String, message: String },
    AbortedError { provider_id: String, message: String },
    StructuredOutputError { provider_id: String, message: String },
    UnknownError { provider_id: String, message: String },
}

4.6 Permission

struct Permission {
    project_id: ProjectId,
    data: Vec<Rule>,      // Ruleset = Vec<Rule>
    time_created: i64,
    time_updated: i64,
}

struct Rule {
    permission: String,   // wildcard-enabled tool name
    pattern: String,      // wildcard-enabled file pattern
    action: Action,
}

4.7 Todo

struct Todo {
    session_id: SessionId,
    content: String,
    status: TodoStatus,   // "pending" | "in_progress" | "completed" | "cancelled"
    priority: Priority,   // "low" | "medium" | "high"
    position: i32,        // ordering
    time_created: i64,
    time_updated: i64,
}

5. APIs & Interfaces

5.1 HTTP Server

The server listens on port 4096 by default (tries 4096, then falls back to OS-assigned port if occupied).

Base URL: http://localhost:4096

All routes except /global/*, /auth/*, and /log require an instance context injected via:

  • Query param ?directory=/path/to/project OR
  • Header X-quickcode-Directory

Defaults to process.cwd() if not provided.

Optional workspace (multi-tenant) via:

  • Query param ?workspace=<id> OR
  • Header X-quickcode-Workspace

Optional auth via quickcode_SERVER_PASSWORD env var (HTTP Basic Auth).

CORS Policy

  • http://localhost:* — allowed
  • http://127.0.0.1:* — allowed
  • tauri://localhost, http://tauri.localhost, https://tauri.localhost — allowed
  • https://*.quickcode.ai — allowed
  • Custom origins via --cors flag

Routes

Auth

PUT  /auth/:providerId    { type, key, ... }   → bool
DEL  /auth/:providerId                         → bool

Project

GET  /project                    → Project[]
GET  /project/current            → Project
POST /project/git/init           → Project
PATCH /project/:projectId        → Project

Session

GET  /session                    → Session[]  (query: projectId, archived)
POST /session                    → Session    (body: projectId, parentId?, title?)
GET  /session/:id                → Session
DELETE /session/:id              → bool
PATCH /session/:id               → Session    (body: title?, permission?)
POST /session/:id/share          → { url }
POST /session/:id/compact        → bool
POST /session/:id/abort          → bool
GET  /session/:id/message        → MessagePage  (query: cursor?)
POST /session/:id/chat           → (SSE stream)

SSE Chat endpointPOST /session/:id/chat:

  • Body: { parts: UserPart[], agent?, model?, system_prompt? }
  • Returns Server-Sent Events stream
  • Events: message parts being created/updated in real-time

Permission

GET  /permission                 → Rule[]
POST /permission                 → Rule      (body: Rule)
DELETE /permission/:id           → bool
GET  /permission/request         → PermissionRequest[]
POST /permission/request/:id     → bool      (body: { allow: bool })

Question

GET  /question                   → Question[]  (pending user-input requests)
POST /question/:id               → bool         (body: { answer: string })

Provider

GET  /provider                   → Provider[]
GET  /provider/:id/models        → Model[]
POST /provider/:id/validate      → bool

Config

GET  /config                     → Config

Commands

GET  /command                    → Command[]

Agents

GET  /agent                      → Agent[]

Skills

GET  /skill                      → Skill[]

LSP

GET  /lsp                        → LspStatus[]

VCS

GET  /vcs                        → { branch, default_branch }
GET  /vcs/diff?mode=             → FileDiff[]

Path

GET  /path                       → { home, state, config, worktree, directory }

Logging

POST /log    { service, level, message, extra? }   → bool

Events (WebSocket/SSE)

GET  /event                     → SSE stream of all bus events

Instance

POST /instance/dispose           → bool

Global routes (no instance context needed)

GET /global/...    Various global endpoints

OpenAPI Spec

GET /doc    → OpenAPI 3.1.1 JSON spec

5.2 Tool Interface

Each tool implements:

trait Tool {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn parameters_schema(&self) -> serde_json::Value;  // JSON Schema
    async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolOutput, ToolError>;
}

5.3 Agent Interface

trait AgentProvider {
    async fn get(name: &str) -> Result<AgentInfo>;
    async fn list() -> Vec<AgentInfo>;
    async fn generate(input: GenerateInput) -> Result<GenerateOutput>;
}

struct GenerateInput {
    session_id: SessionId,
    model: ModelRef,
    schema: Value,      // JSON schema for structured output
    messages: Vec<Message>,
}

struct GenerateOutput {
    value: Value,       // structured output matching schema
}

5.4 LLM Streaming Interface

struct StreamInput {
    session_id: SessionId,
    model: ModelRef,       // provider_id + model_id
    agent: AgentInfo,
    system_prompts: Vec<String>,
    messages: Vec<LlmMessage>,   // converted from MessageV2
    tools: Vec<ToolDefinition>,
    abort: AbortHandle,
    max_tokens: Option<i32>,
    temperature: Option<f64>,
    top_p: Option<f64>,
}

// Returns an async stream of events:
enum StreamEvent {
    Start,
    TextStart { id: String },
    TextDelta { id: String, text: String },
    TextEnd { id: String },
    ReasoningStart { id: String },
    ReasoningDelta { id: String, text: String },
    ReasoningEnd { id: String },
    ToolInputStart { id: String, tool_name: String },
    ToolInputDelta { id: String, delta: String },
    ToolInputEnd { id: String },
    ToolCall { tool_call_id: String, tool_name: String, input: Value },
    ToolResult { tool_call_id: String, input: Value, output: ToolOutput },
    ToolError { tool_call_id: String, input: Value, error: Box<dyn Error> },
    StartStep,
    FinishStep { finish_reason: FinishReason, usage: TokenUsage },
    Finish { finish_reason: FinishReason },
    Error(Box<dyn Error>),
}

6. Execution Flow

6.1 Startup

  1. Parse CLI args (command: run/serve/web, options: port, hostname, log-level, etc.)
  2. Load global configuration from ~/.config/quickcode/config.jsonc
  3. Determine working directory (from arg or cwd)
  4. Start HTTP server on configured port
  5. If TUI mode: Start TUI, render UI, connect to server
  6. If serve mode: Start server only, print URL

6.2 Instance Bootstrap

When the first request arrives for a directory:

  1. Canonicalize the directory path
  2. Look up or create Project record in SQLite
  3. Initialize VCS context (detect git, find worktree root)
  4. Start LSP servers for languages detected in the project
  5. Load project-local config (.quickcode/quickcode.jsonc)
  6. Initialize snapshot tracker

6.3 Chat Message Flow

User sends message
       │
       ▼
Create UserMessage + TextPart in DB
       │
       ▼
Create AssistantMessage (empty) in DB
       │
       ▼
Run SessionProcessor.process():
  ┌─── Start LLM stream ───┐
  │                          │
  │  for each stream event:  │
  │    - text delta → update TextPart in DB, publish event
  │    - reasoning delta → update ReasoningPart in DB
  │    - tool-input-start → create ToolPart (pending) in DB
  │    - tool-call → update ToolPart (running), check permissions, execute tool
  │    - tool-result → update ToolPart (completed) with output
  │    - tool-error → update ToolPart (error)
  │    - step-finish → compute token usage, save cost
  │    - finish → done
  │                          │
  └──────────────────────────┘
       │
       ▼
Check result:
  - "continue" → done
  - "compact" → run compaction, then re-run
  - "stop" → done (blocked by permission denial)

6.4 Tool Execution Flow

LLM emits tool-call event
       │
       ▼
Resolve tool from registry
       │
       ▼
Parse input args (validate schema)
       │
       ▼
Check permissions:
  ctx.ask(permission, pattern, ruleset)
  - evaluate rules → action
  - "allow" → proceed
  - "deny" → throw PermissionDeniedError
  - "ask" → send PermissionRequest, await UI/API response
       │
       ▼
Execute tool.execute(args, ctx)
       │
       ▼
Return ToolOutput to LLM stream

6.5 Permission Request Flow

Tool needs permission
       │
       ▼
Create PermissionRequest { id, session_id, permission, patterns, metadata }
       │
       ▼
Store in memory (pending requests map)
       │
       ▼
Publish "permission.requested" bus event → TUI shows prompt / API exposes endpoint
       │
       ▼
Wait for user response (allow/deny)
       │
       ▼
If allow: proceed, optionally save rule to session/global config
If deny: throw PermissionRejectedError (tool gets error state)

6.6 Compaction Flow

Triggered when:

  • LLM response indicates context overflow error
  • finish-step usage tokens exceed model's context limit threshold
Collect all messages in session
       │
       ▼
Find messages to compact (all except last N)
       │
       ▼
Run "compaction" agent:
  - System prompt: summarize the conversation
  - Include all old messages
  - Generate summary text
       │
       ▼
Store summary in a CompactionPart
       │
       ▼
Mark old messages as "summarized"
       │
       ▼
Future requests only send summary + recent messages

6.7 Snapshot / Revert Flow

Snapshot tracking (per step):

  1. At start-step: call Snapshot.track() → returns a snapshot ID (git stash)
  2. At finish-step: call Snapshot.patch(snapshot_id) → returns list of changed files with diffs
  3. Store patch in PatchPart for that message

Revert:

  1. User requests revert to a specific message_id
  2. Find the SnapshotPart before that message
  3. Apply reverse of patches to restore files
  4. Update session's revert metadata

7. Business Logic

7.1 Model Resolution

Models are specified as provider_id/model_id strings. Resolution:

  1. Look up provider by provider_id
  2. Look up model in provider's model catalog
  3. If model has variants (e.g., different context sizes), select appropriate variant
  4. Apply model-specific options (headers, parameters)

For Bedrock, prefix model ID with region prefix based on AWS region:

  • us-* regions → us. prefix for cross-region inference
  • eu-*eu. prefix
  • ap-northeast-*jp. prefix
  • etc.

7.2 Context Window Management

Before each LLM request:

  1. Calculate total tokens in message history
  2. If approaching model's context limit, trigger compaction
  3. After compaction, include: system prompt + compaction summary + recent N messages

7.3 Doom Loop Detection

The doom loop detector prevents the LLM from calling the same tool with the same args repeatedly:

  1. After each tool call, check the last 3 tool parts in the current message
  2. If all 3 are the same tool with identical input, ask user for permission to continue
  3. If user denies, stop the loop

Threshold: 3 identical consecutive tool calls.

7.4 Tool Filtering by Model

Not all tools are available for all models:

  • codesearch and websearch tools: only available for the quickcode provider or when explicitly enabled
  • apply_patch tool: preferred for GPT models (OpenAI)
  • edit tool: preferred for Claude models

Tool selection logic in registry determines which tools to expose to the LLM based on provider.

7.5 Session Title Generation

After the first assistant message completes:

  1. Run "title" agent (subagent) with the conversation so far
  2. Generate a short title string (≤ 50 chars)
  3. Update session title in DB and publish update event

7.6 Session Summary Generation

After each step completes with file changes:

  1. Run "summary" agent asynchronously
  2. Compute additions, deletions, file count from diffs
  3. Update session summary field in DB

7.7 Permission Rule Evaluation

Rules are matched using wildcard patterns (not regexes). The wildcard matching algorithm:

  • * matches any sequence of characters (not across path separators for file paths)
  • ** matches across path separators

Last matching rule wins. If no rule matches, action is ask.

Multiple rulesets are merged (flattened) in priority order:

  1. Global config rules
  2. Session-level rules
  3. Agent permission rules

7.8 File Time Tracking

Tools that modify files track a "file time" — the last-known modification time. Before writing/editing:

  1. Check current file mtime against tracked mtime
  2. If file was modified externally, surface a warning or error

7.9 Retry Logic

When LLM calls fail, certain errors trigger automatic retry with exponential backoff:

Retryable errors:

  • Rate limit errors (429)
  • Temporary API errors (5xx)
  • Network timeouts

Non-retryable:

  • Auth errors (401, 403)
  • Context overflow
  • Output length exceeded

Retry state is stored as a RetryPart in the message and published via bus events so the UI can show retry countdown.

7.10 LSP Diagnostics on File Write

After writing/editing files:

  1. Wait briefly for LSP to process changes (debounce)
  2. Fetch diagnostics from LSP
  3. Attach diagnostics as tool output metadata (up to 20 errors per file, max 5 files)
  4. Include project-wide errors if present

7.11 VCS (Git) Integration

Vcs module wraps git operations:

  • branch() → current branch name
  • default_branch() → main/master/trunk
  • diff(mode) → file diffs
    • mode = "working_tree": uncommitted changes
    • mode = "default_branch": diff against default branch

8. External Dependencies

8.1 Rust Crate Equivalents

Original (TypeScript) Purpose Rust Equivalent
hono HTTP framework axum
effect DI / async effects tokio + trait objects
drizzle-orm + SQLite ORM sqlx with SQLite
zod Schema validation serde + validator
ai (Vercel AI SDK) LLM streaming Custom HTTP client / async-openai + provider-specific
tree-sitter Code parsing tree-sitter (has Rust bindings)
ripgrep (rg) File search ignore crate + regex OR shell rg
shiki / syntax highlighting Code rendering syntect
marked Markdown rendering pulldown-cmark
fuzzysort Fuzzy search fuzzy-matcher
turndown HTML→Markdown htmd or custom
chokidar File watching notify
EventEmitter Pub/sub tokio::sync::broadcast
jsonc-parser JSONC parsing jsonc-parser (Rust) or custom
remeda Functional utils Standard Rust iterators
bun-pty PTY/shell portable-pty
@modelcontextprotocol/sdk MCP client/server Custom or mcp-rs

8.2 External Services

Service Purpose Integration
models.dev Model catalog HTTP GET https://models.dev/api.json, cached locally, refreshed hourly
Anthropic API LLM https://api.anthropic.com
OpenAI API LLM https://api.openai.com
Google Gemini LLM https://generativelanguage.googleapis.com
Azure OpenAI LLM Per-resource URL
Amazon Bedrock LLM AWS SDK
OpenRouter LLM proxy https://openrouter.ai/api
Exa Web search https://mcp.exa.ai/mcp (MCP-over-SSE protocol)
GitHub Copilot LLM https://api.githubcopilot.com
GitLab AI LLM GitLab instance URL

8.3 System Dependencies

  • git CLI — must be present for VCS features
  • rg (ripgrep) — used by glob and grep tools
  • Language server binaries (optional, detected at runtime):
    • typescript-language-server (TypeScript/JavaScript)
    • pylsp / pyright (Python)
    • rust-analyzer (Rust)
    • gopls (Go)
    • etc.

9. Configuration

9.1 Config File Format

Files are JSONC (JSON with comments + trailing commas). Location search:

  1. $quickcode_HOME/config.jsonc (default ~/.config/quickcode/config.jsonc)
  2. .quickcode/quickcode.jsonc files from project dir up to worktree root
  3. ~/.quickcode/quickcode.jsonc
  4. $quickcode_CONFIG_DIR/quickcode.jsonc if env var set

9.2 Config Schema

{
  // Permission rules (last-wins evaluation)
  "permission": [
    {
      "permission": "bash",
      "pattern": "*",
      "action": "allow"    // "allow" | "deny" | "ask"
    }
  ],

  // Provider configurations
  "provider": {
    "anthropic": {
      "api_key": "{env:ANTHROPIC_API_KEY}",
      // or: "{file:~/.anthropic_key}"
    }
  },

  // Model overrides
  "model": "anthropic/claude-sonnet-4-5",

  // Custom commands (slash commands)
  "command": {
    "my_command": {
      "description": "Run my custom command",
      "run": "bash script here"
    }
  },

  // MCP server configurations
  "mcp": {
    "server_name": {
      "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem"],
      "args": ["/path"],
      "env": {}
    }
  },

  // Agent overrides
  "agent": {
    "build": {
      "model": "openai/gpt-4o",
      "temperature": 0.7
    }
  },

  // Experimental features
  "experimental": {
    "continue_loop_on_deny": false
  }
}

9.3 Environment Variables

Variable Purpose Default
quickcode_HOME Override home/data directory ~/.local/share/quickcode
quickcode_CONFIG_DIR Additional config directory
quickcode_SERVER_PASSWORD Enable HTTP Basic Auth
quickcode_SERVER_USERNAME HTTP Basic Auth username quickcode
quickcode_MODELS_URL Override models.dev URL https://models.dev
quickcode_MODELS_PATH Use local models JSON file
quickcode_DISABLE_MODELS_FETCH Skip models.dev refresh
quickcode_DISABLE_PROJECT_CONFIG Skip project config files
ANTHROPIC_API_KEY Anthropic auth
OPENAI_API_KEY OpenAI auth
GOOGLE_API_KEY Google auth
AWS_ACCESS_KEY_ID, etc. AWS Bedrock auth

9.4 Directory Structure

$quickcode_HOME/           (default: ~/.local/share/quickcode/)
├── quickcode.db           # SQLite database
├── config.jsonc          # User config
├── log/                  # Log files
├── cache/
│   └── models.json       # Cached models.dev data
├── plugin/               # Custom plugins (.ts/.js files)
├── skill/                # Custom skills (markdown files)
└── tool/                 # Custom tools

10. Error Handling

10.1 Error Types

// Base error type - all named errors carry a name + structured data
struct NamedError {
    name: String,
    message: String,
    data: Value,
}

// Common error variants
struct NotFoundError { message: String }
struct PermissionRejectedError { permission: String, pattern: String }
struct WorktreeError { message: String }
struct ModelNotFoundError { model_id: String, provider_id: String }
struct ProviderAuthValidationFailed { provider_id: String, message: String }
struct ConfigJsonError { path: String, message: String }
struct ConfigInvalidError { path: String, issues: Vec<ValidationIssue>, message: String }

10.2 HTTP Error Mapping

Error HTTP Status
NotFoundError 404
ModelNotFoundError 400
ProviderAuthValidationFailed 400
WorktreeError* 400
All other NamedError 500
Unknown errors 500 with stack trace

10.3 Tool Error Handling

When a tool throws:

  • PermissionRejectedError → tool part state = error; if continue_loop_on_deny is false, stop the agent loop
  • QuestionRejectedError → same as permission rejected
  • Other errors → tool part state = error; agent loop continues (LLM sees error result)

10.4 LLM Error Handling

Error Handling
Rate limit (429) Exponential backoff, retry up to N times
Server error (5xx) Retry with backoff
Auth error (401) No retry, surface AuthError
Context overflow Trigger compaction, retry
Output length Store OutputLengthError, stop
Abort signal Store AbortedError, stop cleanly

10.5 Structured Output Errors

When using structured output (JSON schema) and the model returns invalid JSON:

  • Store StructuredOutputError
  • Do not retry automatically
  • Surface to user

11. Edge Cases

11.1 File System

  • Binary files: Detect by extension (.png, .jpg, .pdf, etc.) and by reading first bytes. Return base64-encoded content as attachment rather than text.
  • Large files: Truncate output at ~50KB per read, ~30KB for bash output
  • Long lines: Truncate at 2000 chars per line
  • External paths: Files outside instance.directory and instance.worktree require external_directory permission
  • Non-git projects: worktree is set to /; skip worktree boundary checks to avoid matching all paths
  • Symlinks: ripgrep handles broken symlinks gracefully in grep/glob tools

11.2 Permission System

  • Wildcard matching: Both permission AND pattern must match for a rule to apply
  • Non-git worktree at /: Do NOT use worktree as path boundary (would allow everything)
  • "always" rules: Some permissions have an "always" list — these patterns bypass the ask/deny logic for that permission type
  • Doom loop permission: Special doom_loop permission type, always auto-creates ask dialog

11.3 LLM Behavior

  • Interleaved reasoning: Some models (Claude, o1) emit reasoning tokens between tool calls; handle reasoning-start/delta/end events
  • LiteLLM proxy: When using LiteLLM, add a dummy tool if no tools provided (some models require at least one tool)
  • GitLab workflow models: Use toolExecutor pattern instead of standard tool streaming
  • Multiple step loops: Each start-step/finish-step pair is one "step"; a single message can have multiple steps
  • Token cost calculation: Some providers report usage in providerMetadata rather than standard fields; check both

11.4 Compaction

  • Summarization agent: Uses the compaction agent which has lower capabilities
  • Preserved messages: Always keep the most recent N messages (configured); only compact older ones
  • Re-compaction: If after compaction the session is still too large, compact again
  • Summary messages: When displaying compacted sessions, show a CompactionPart indicating what was summarized

11.5 Session Forking

  • Fork creates a new session with parent_id set
  • All messages from parent are copied to child
  • Changes in forked session don't affect parent
  • Used for branching conversations

11.6 Task Delegation

  • Subtasks create child sessions with parent_id
  • Child session inherits permissions but with explicit denials for "task" and "todowrite" tools (prevents recursive delegation loops)
  • Subtask result is returned to parent session as tool output

11.7 Config Substitution

  • {env:VARNAME} → replaced with env var value, empty string if not set
  • {file:path} → replaced with file contents (JSON-escaped)
  • Relative file paths are resolved relative to the config file's directory
  • ~/ is expanded to home directory
  • Lines starting with // that contain {file:} tokens are treated as comments (substitution skipped)

11.8 Model Variants

Some models have multiple variants (e.g., different context windows). The variant selection logic:

  1. Check model's variants map
  2. Select based on configured option or default
  3. Merge variant's options into the base model options

12. Testing Requirements

12.1 Unit Tests

Permission evaluation:

  • Test wildcard matching for various permission/pattern combinations
  • Verify last-rule-wins behavior
  • Verify merge of multiple rulesets

Config parsing:

  • JSONC parsing with comments and trailing commas
  • {env:VAR} substitution
  • {file:path} substitution with relative/absolute/home paths
  • Error reporting with line/column numbers

Message conversion:

  • MessageV2 → LLM SDK messages conversion
  • Multi-turn conversation ordering
  • Tool call/result pairing
  • File attachment handling (images, PDFs)

Token usage calculation:

  • Cost computation using model pricing data
  • Cache read/write token accounting
  • Reasoning token accounting

Doom loop detection:

  • 3 identical consecutive tool calls triggers ask
  • Different args don't trigger
  • Different tool names don't trigger

12.2 Integration Tests

Session lifecycle:

  • Create session → send message → receive streamed response → verify parts in DB
  • Fork session → verify messages copied
  • Archive session → verify not returned in default list

Tool execution:

  • bash tool: execute a simple command, verify output
  • read tool: read a file, verify content
  • write tool: write a file, verify on disk
  • edit tool: edit file with various replace strategies, verify result
  • glob tool: find files by pattern, verify results
  • grep tool: search content, verify matches

Permission flow:

  • Tool request → permission ask → user allows → tool executes
  • Tool request → permission ask → user denies → PermissionRejectedError
  • Saved allow rule → auto-allow on next request

Compaction:

  • Fill session beyond context limit → trigger compaction → verify summary stored
  • Verify next message uses compacted history

12.3 API Tests

HTTP server:

  • All REST endpoints return correct status codes
  • Session SSE stream delivers events in correct order
  • WebSocket event stream delivers bus events

Auth:

  • PUT /auth/:id stores credentials
  • DELETE /auth/:id removes credentials
  • Missing credentials return appropriate error

12.4 Behavior Validation

LLM streaming:

  • Text parts are streamed incrementally
  • Tool calls are properly paired with results
  • Retry behavior on rate limits
  • Abort handling mid-stream

Session prompt construction: The prompt sent to LLM must include:

  1. Agent system prompt
  2. Provider-specific instructions (if any)
  3. Custom user system prompts (from config)
  4. Tool availability instructions
  5. Conversation history (user messages + assistant messages with tool results)

When converting message history to LLM format:

  • TextPart → text content
  • ToolPart (completed) → tool_use block + tool_result block
  • FilePart → file attachment
  • Skipped: StepStart, StepFinish, Patch, Snapshot, Compaction (internal metadata only)
  • Summary messages → special "compaction" content block

Appendix A: ID Schemes

All IDs use ascending ULIDs (sortable by time):

ProjectId: "p_" + hash(directory)[0..8]
SessionId: ascending ULID with "s_" prefix
MessageId: ascending ULID with "m_" prefix
PartId:    ascending ULID with "pt_" prefix
PermissionId: ascending ULID with "permission_" prefix

Ascending ULID format: <prefix><timestamp_ms_base32><random_base32> — lexicographically sortable, time-ordered.


Appendix B: Key Constants

Constant Value Purpose
Default server port 4096 HTTP API listen port
Max bash output 30 KB Truncate bash tool output
Max read output 50 KB Truncate file read output
Max line length 2000 chars Truncate long lines
Max glob results 100 files Limit glob tool results
Max grep results 100 matches Limit grep tool results
Max read lines 2000 Default read limit
Web fetch max size 5 MB HTTP response size limit
Web fetch default timeout 30 seconds HTTP timeout
Web fetch max timeout 120 seconds Max configurable timeout
Bash default timeout 120 seconds (2 min) Shell command timeout
Doom loop threshold 3 Identical tool calls before asking
Models refresh interval 1 hour Periodic models.dev refresh
Models fetch timeout 10 seconds models.dev HTTP timeout
Web search results 8 (default) Number of search results
Web search timeout 25 seconds Exa API timeout

Appendix C: Wildcard Matching Algorithm

The permission system uses wildcard matching, NOT glob or regex:

fn wildcard_match(text: &str, pattern: &str) -> bool {
    // '*' matches any sequence of characters
    // '?' matches any single character (if implemented)
    // Pattern "*" matches everything
    // Patterns do NOT use path-separator-awareness (unlike glob)
}

Both the permission field AND the pattern field of a rule must match for the rule to apply:

  • Permission matching: tool name (e.g., bash, edit, *)
  • Pattern matching: file path or resource identifier

Appendix D: Database Schema (SQLite)

CREATE TABLE project (
    id TEXT PRIMARY KEY,
    worktree TEXT NOT NULL,
    vcs TEXT,
    name TEXT,
    icon_url TEXT,
    icon_color TEXT,
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    time_initialized INTEGER,
    sandboxes TEXT NOT NULL DEFAULT '[]',  -- JSON array
    commands TEXT                           -- JSON object
);

CREATE TABLE session (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES project(id) ON DELETE CASCADE,
    workspace_id TEXT,
    parent_id TEXT,
    slug TEXT NOT NULL,
    directory TEXT NOT NULL,
    title TEXT NOT NULL,
    version TEXT NOT NULL,
    share_url TEXT,
    summary_additions INTEGER,
    summary_deletions INTEGER,
    summary_files INTEGER,
    summary_diffs TEXT,           -- JSON array of FileDiff
    revert TEXT,                  -- JSON object
    permission TEXT,              -- JSON array of Rule
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    time_compacting INTEGER,
    time_archived INTEGER
);

CREATE INDEX session_project_idx ON session(project_id);
CREATE INDEX session_workspace_idx ON session(workspace_id);
CREATE INDEX session_parent_idx ON session(parent_id);

CREATE TABLE message (
    id TEXT PRIMARY KEY,
    session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    data TEXT NOT NULL             -- JSON: MessageInfo (role, agent, model, etc.)
);

CREATE INDEX message_session_time_created_id_idx ON message(session_id, time_created, id);

CREATE TABLE part (
    id TEXT PRIMARY KEY,
    message_id TEXT NOT NULL REFERENCES message(id) ON DELETE CASCADE,
    session_id TEXT NOT NULL,
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    data TEXT NOT NULL             -- JSON: PartData (type-discriminated union)
);

CREATE INDEX part_message_id_id_idx ON part(message_id, id);
CREATE INDEX part_session_idx ON part(session_id);

CREATE TABLE todo (
    session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    status TEXT NOT NULL,
    priority TEXT NOT NULL,
    position INTEGER NOT NULL,
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    PRIMARY KEY (session_id, position)
);

CREATE INDEX todo_session_idx ON todo(session_id);

CREATE TABLE permission (
    project_id TEXT PRIMARY KEY REFERENCES project(id) ON DELETE CASCADE,
    time_created INTEGER NOT NULL,
    time_updated INTEGER NOT NULL,
    data TEXT NOT NULL              -- JSON array of Rule
);

Appendix E: Provider Model Reference Format

Models are referenced as provider_id/model_id strings. Examples:

  • anthropic/claude-sonnet-4-5
  • openai/gpt-4o
  • google/gemini-2.0-flash
  • bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0
  • openrouter/anthropic/claude-3.5-sonnet

The model catalog from models.dev provides:

  • Input/output costs per token
  • Context window size
  • Output token limit
  • Supported modalities (text, image, audio, video, pdf)
  • Feature flags: attachment, reasoning, temperature, tool_call, interleaved

Appendix F: LLM Message Format for API Calls

When sending messages to LLM providers, convert MessageV2 to the provider-specific format:

User message:
  - TextPart → { role: "user", content: [{ type: "text", text: "..." }] }
  - FilePart → { type: "image" or "file", ... } depending on media type

Assistant message:
  - TextPart → { type: "text", text: "..." }
  - ToolPart(Completed) → { type: "tool_use", id: call_id, name: tool, input: ... }
                         + following user message: { type: "tool_result", tool_use_id: call_id, content: output }
  - ReasoningPart → { type: "thinking", thinking: text } (Anthropic-specific)

  Skip: StepStart, StepFinish, Patch, Snapshot, Subtask, Agent, Retry parts

Compaction summary:
  - Injected as user/assistant exchange with the summary text
  - Old messages before the compaction point are excluded

End of Specification