Derived from full analysis of the quickcode TypeScript codebase. Target: A Rust implementation with identical behavior, named
quickcode.
- Overview
- Architecture
- Core Modules
- Data Models
- APIs & Interfaces
- Execution Flow
- Business Logic
- External Dependencies
- Configuration
- Error Handling
- Edge Cases
- Testing Requirements
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.
- 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)
- TUI mode (default): interactive terminal application
- Serve mode: headless HTTP API server, clients connect via SDK or browser
- Web mode: proxy to web app, serves browser-based UI
┌─────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────────────────┘
| 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 |
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.
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.deletedsession.diff(file changes)session.errorserver.connected,global.disposedserver.instance.disposed
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)→Sessionfork(session_id)→Session(copies all messages)archive(session_id)— soft-delete, setstime_archivedsend_message(session_id, user_input, model, agent)→ async streamcompact(session_id)— run compaction to reduce token countshare(session_id)→ URLrevert(session_id, message_id)— restore filesystem to snapshot at that pointlist(project_id)→Vec<Session>
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 identifierdescription: displayed in UImode: "primary" | "subagent" | "all"permission: optional permission ruleset overridemodel: optional model override (provider/model string)temperature,top_p: optional model paramsnative: bool (built-in vs user-defined)hidden: bool (not shown in UI)
Each tool is defined with:
name: string identifier (used in LLM tool calls)description: shown to the LLMparameters: 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.
}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.
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:
- Merge rulesets (global config → session config → agent config)
- Find last matching rule (both
permissionANDpatternmust match via wildcard) - 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 executionexternal_directory— file access outside project dirlsp— LSP operations (always allow with wildcard pattern)doom_loop— repeated identical tool calls (loop detection)plan_exit— exiting plan mode
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.
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):
~/.config/quickcode/config.jsonc(global user config).quickcode/quickcode.jsoncfiles walking up from project dir to worktree root~/.quickcode/quickcode.jsoncquickcode_CONFIG_DIRenv var if set
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
}In-process typed pub/sub. Events are typed by a string type + payload schema. Subscriptions can be callback-based or stream-based.
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
SQLite database using sqlx (Rust). Tables:
projectsessionmessageparttodopermission
Database location: ~/.local/share/quickcode/quickcode.db (or quickcode_HOME override).
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_.
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>,
}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>,
}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 }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 },
}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,
}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,
}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/projectOR - 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).
http://localhost:*— allowedhttp://127.0.0.1:*— allowedtauri://localhost,http://tauri.localhost,https://tauri.localhost— allowedhttps://*.quickcode.ai— allowed- Custom origins via
--corsflag
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 endpoint — POST /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
GET /doc → OpenAPI 3.1.1 JSON spec
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>;
}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
}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>),
}- Parse CLI args (command: run/serve/web, options: port, hostname, log-level, etc.)
- Load global configuration from
~/.config/quickcode/config.jsonc - Determine working directory (from arg or
cwd) - Start HTTP server on configured port
- If TUI mode: Start TUI, render UI, connect to server
- If serve mode: Start server only, print URL
When the first request arrives for a directory:
- Canonicalize the directory path
- Look up or create
Projectrecord in SQLite - Initialize VCS context (detect git, find worktree root)
- Start LSP servers for languages detected in the project
- Load project-local config (
.quickcode/quickcode.jsonc) - Initialize snapshot tracker
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)
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
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)
Triggered when:
- LLM response indicates context overflow error
finish-stepusage 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
Snapshot tracking (per step):
- At
start-step: callSnapshot.track()→ returns a snapshot ID (git stash) - At
finish-step: callSnapshot.patch(snapshot_id)→ returns list of changed files with diffs - Store patch in
PatchPartfor that message
Revert:
- User requests revert to a specific
message_id - Find the
SnapshotPartbefore that message - Apply reverse of patches to restore files
- Update session's
revertmetadata
Models are specified as provider_id/model_id strings. Resolution:
- Look up provider by
provider_id - Look up model in provider's model catalog
- If model has variants (e.g., different context sizes), select appropriate variant
- 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 inferenceeu-*→eu.prefixap-northeast-*→jp.prefix- etc.
Before each LLM request:
- Calculate total tokens in message history
- If approaching model's context limit, trigger compaction
- After compaction, include: system prompt + compaction summary + recent N messages
The doom loop detector prevents the LLM from calling the same tool with the same args repeatedly:
- After each tool call, check the last 3 tool parts in the current message
- If all 3 are the same tool with identical input, ask user for permission to continue
- If user denies, stop the loop
Threshold: 3 identical consecutive tool calls.
Not all tools are available for all models:
codesearchandwebsearchtools: only available for thequickcodeprovider or when explicitly enabledapply_patchtool: preferred for GPT models (OpenAI)edittool: preferred for Claude models
Tool selection logic in registry determines which tools to expose to the LLM based on provider.
After the first assistant message completes:
- Run "title" agent (subagent) with the conversation so far
- Generate a short title string (≤ 50 chars)
- Update session title in DB and publish update event
After each step completes with file changes:
- Run "summary" agent asynchronously
- Compute additions, deletions, file count from diffs
- Update session
summaryfield in DB
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:
- Global config rules
- Session-level rules
- Agent permission rules
Tools that modify files track a "file time" — the last-known modification time. Before writing/editing:
- Check current file mtime against tracked mtime
- If file was modified externally, surface a warning or error
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.
After writing/editing files:
- Wait briefly for LSP to process changes (debounce)
- Fetch diagnostics from LSP
- Attach diagnostics as tool output metadata (up to 20 errors per file, max 5 files)
- Include project-wide errors if present
Vcs module wraps git operations:
branch()→ current branch namedefault_branch()→ main/master/trunkdiff(mode)→ file diffsmode = "working_tree": uncommitted changesmode = "default_branch": diff against default branch
| 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 |
| 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 |
gitCLI — must be present for VCS featuresrg(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.
Files are JSONC (JSON with comments + trailing commas). Location search:
$quickcode_HOME/config.jsonc(default~/.config/quickcode/config.jsonc).quickcode/quickcode.jsoncfiles from project dir up to worktree root~/.quickcode/quickcode.jsonc$quickcode_CONFIG_DIR/quickcode.jsoncif env var set
| 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 | — |
$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
// 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 }| Error | HTTP Status |
|---|---|
NotFoundError |
404 |
ModelNotFoundError |
400 |
ProviderAuthValidationFailed |
400 |
WorktreeError* |
400 |
All other NamedError |
500 |
| Unknown errors | 500 with stack trace |
When a tool throws:
PermissionRejectedError→ tool part state = error; ifcontinue_loop_on_denyis false, stop the agent loopQuestionRejectedError→ same as permission rejected- Other errors → tool part state = error; agent loop continues (LLM sees error result)
| 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 |
When using structured output (JSON schema) and the model returns invalid JSON:
- Store
StructuredOutputError - Do not retry automatically
- Surface to user
- 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.directoryandinstance.worktreerequireexternal_directorypermission - Non-git projects:
worktreeis set to/; skip worktree boundary checks to avoid matching all paths - Symlinks: ripgrep handles broken symlinks gracefully in grep/glob tools
- Wildcard matching: Both
permissionANDpatternmust 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_looppermission type, always auto-creates ask dialog
- Interleaved reasoning: Some models (Claude, o1) emit reasoning tokens between tool calls; handle
reasoning-start/delta/endevents - LiteLLM proxy: When using LiteLLM, add a dummy tool if no tools provided (some models require at least one tool)
- GitLab workflow models: Use
toolExecutorpattern instead of standard tool streaming - Multiple step loops: Each
start-step/finish-steppair is one "step"; a single message can have multiple steps - Token cost calculation: Some providers report usage in
providerMetadatarather than standard fields; check both
- Summarization agent: Uses the
compactionagent 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
CompactionPartindicating what was summarized
- Fork creates a new session with
parent_idset - All messages from parent are copied to child
- Changes in forked session don't affect parent
- Used for branching conversations
- 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
{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)
Some models have multiple variants (e.g., different context windows). The variant selection logic:
- Check model's
variantsmap - Select based on configured option or default
- Merge variant's options into the base model options
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
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:
bashtool: execute a simple command, verify outputreadtool: read a file, verify contentwritetool: write a file, verify on diskedittool: edit file with various replace strategies, verify resultglobtool: find files by pattern, verify resultsgreptool: 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
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/:idstores credentialsDELETE /auth/:idremoves credentials- Missing credentials return appropriate error
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:
- Agent system prompt
- Provider-specific instructions (if any)
- Custom user system prompts (from config)
- Tool availability instructions
- Conversation history (user messages + assistant messages with tool results)
When converting message history to LLM format:
TextPart→ text contentToolPart(completed) → tool_use block + tool_result blockFilePart→ file attachment- Skipped: StepStart, StepFinish, Patch, Snapshot, Compaction (internal metadata only)
- Summary messages → special "compaction" content block
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.
| 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 |
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
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
);Models are referenced as provider_id/model_id strings. Examples:
anthropic/claude-sonnet-4-5openai/gpt-4ogoogle/gemini-2.0-flashbedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0openrouter/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
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
{ // 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 } }