OpenCode — Deep Dive Knowledge Base
Reverse-engineered analysis of the OpenCode codebase: architecture, orchestration, tools, database, and design philosophy.
Shared from "opencode-study" on Inkdown
Reverse-engineered analysis of the OpenCode codebase: architecture, orchestration, tools, database, and design philosophy.
OpenCode is an AI-powered coding assistant for local development. It runs entirely on your machine — no cloud dependency for code access — giving the LLM full codebase context.
Primary use cases:
Target audience: Software developers (full-stack, backend, frontend, DevOps, open-source contributors) who want AI assistance wired into their daily workflow with local control and safety.
opencode/
├── packages/ # 13 packages total
│ ├── opencode/ # Core business logic, CLI, server
│ │ ├── src/
│ │ │ ├── agent/ # Agent definitions, permissions
│ │ │ ├── session/ # Session management, LLM loop
│ │ │ ├── tool/ # Built-in tool implementations
│ │ │ ├── config/ # Configuration loading
│ │ │ ├── auth/ # Authentication (OAuth, API keys)
│ │ │ ├── server/ # HTTP API + WebSocket server
│ │ │ ├── mcp/ # MCP client integration
│ │ │ ├── storage/ # Drizzle ORM + SQLite schemas
│ │ │ ├── permission/ # Permission evaluation engine
│ │ │ ├── question/ # Human-in-loop question system
│ │ │ ├── background/ # Background job system
│ │ │ └── effect/ # Effect utilities
│ ├── core/ # Shared utilities (logging, filesystem, schemas)
│ ├── llm/ # LLM provider abstraction + tool runtime
│ ├── sdk/ # JavaScript/TypeScript SDK for API consumers
│ ├── plugin/ # Plugin system
│ ├── app/ # Shared SolidJS UI components
│ ├── desktop/ # Electron desktop app
│ ├── ui/ # UI component library
│ ├── web/ # Web documentation site
│ └── console/ # Admin console
├── script/ # Build scripts and release automation
└── migration/ # SQLite migration filesPattern: Service-Oriented with Effect (functional programming)
Every feature is a Service with three parts:
| Part | Purpose |
|---|---|
| Interface | Defines what the service can do |
| Implementation | How it does it |
| Layer | How to inject its dependencies |
Core services:
| Service | Responsibility |
|---|---|
| Session | Manages AI conversations |
| Agent | AI personalities (build, plan, explore, etc.) |
| Config | Loads settings from files / env / remote |
| Auth | API keys and OAuth handling |
| Provider | Connects to LLM providers (OpenAI, Anthropic, etc.) |
| Tool | File ops, shell, web, orchestration |
| Server | HTTP API for headless operation |
| Permission | Evaluates and enforces tool access rules |
| Question | Structured human-in-loop prompts |
| Background | Manages async/background job execution |
Data flow:
User input (CLI/TUI/Web/Desktop)
→ Config loaded
→ Session created → Agent selected
→ Agent uses Tools → LLM called via Provider
→ Results stored in SQLite
→ Events emitted (Created, Updated, Deleted, Diff, Error)Key technologies:
| Layer | Technology |
|---|---|
| Language | TypeScript |
| Functional core | Effect |
| Validation | Effect Schema |
| UI | SolidJS |
| ORM | Drizzle ORM |
| Database | SQLite |
| Runtime & build | Bun |
What they built themselves:
task toolWhat external libraries handle (infrastructure, not orchestration):
| Library | Role |
|---|---|
| Effect | FP foundation (like React for UI — structural, not orchestration) |
| Vercel AI SDK | Optional provider transport adapter (one of two runtime options) |
| Drizzle | Database persistence |
packages/llm/src/route/)Route.make({
protocol: Protocol, // API contract (OpenAI, Anthropic, etc.)
endpoint: Endpoint, // URL construction
auth: Auth, // Authentication (bearer, headers, signing)
framing: Framing // Stream parsing (SSE, event-stream)
})One protocol works across many providers — protocol-agnostic routing.
packages/llm/src/tool-runtime.ts)Tool.define(id, Effect.succeed({
description: string,
parameters: Schema, // Effect Schema for validation
execute: (args, ctx) => Effect.Effect<ExecuteResult>
}))packages/opencode/src/session/llm.ts)LLM Request → Route.compile → Provider API
↓
Tool Call Event detected
↓
ToolRuntime.execute
↓
Tool.execute (Effect)
↓
Tool Result → Fed back to LLMEach agent has:
{
name: "explore",
mode: "subagent", // "primary" = user-facing, "subagent" = helper only
permission: {
"*": "deny", // Default deny everything
grep: "allow",
glob: "allow",
read: "allow",
},
prompt: "You are a fast exploration agent...",
model: { ... },
temperature: 0.0
}| Agent | Mode | Purpose | Key tools |
|---|---|---|---|
build | primary | Default — executes tools | all permitted tools |
plan | primary | Planning mode | no edit tools |
general | primary | Multi-step parallel tasks | broad tool set |
explore | subagent | Fast codebase exploration | grep, glob, read only |
scout | subagent | External docs & dependency research (experimental) | webfetch, websearch |
compaction | subagent | Context compaction | read |
title | subagent | Generate session titles | none |
summary | subagent | Summarise sessions | read |
User input → session processor → agent selected (based on config/request) → LLM called with agent's prompt + available tools
Sub-agents are not picked from a queue. The parent agent explicitly spawns them by calling the task tool:
task({
description: "Explore codebase",
prompt: "Find all API endpoints",
subagent_type: "explore"
})This creates a new independent session:
sessions.create({
parentID: ctx.sessionID, // Link to parent
title: description + " (@explore subagent)",
permission: derivedPermissions // Merged from parent + subagent
})src/agent/subagent-permissions.ts merges permissions in this order:
Parent agent's edit denies
+ Parent session's denies
+ Parent session's external_directory rules
+ Default denies (todowrite, task unless explicitly allowed)
= Sub-agent's effective permission setSub-agents cannot have more permissions than their parent. This prevents permission escalation.
Parent calls task tool
→ New session created with derived permissions
→ Subagent processes its own prompt independently
→ Result returned to parent as text
→ Parent can continue or resume the subagent (via task_id)| Mode | Behaviour |
|---|---|
| Synchronous | Parent waits for subagent to complete |
Background (background: true) | Subagent runs async, parent continues immediately (experimental) |
| Resume | Pass task_id to continue same subagent session |
No hard limit in code. Constrained by:
ask)Tools are merged from three sources in the registry:
const tools = yield* Effect.all({
builtIn: ToolRegistry.builtIn(), // Custom tools
plugin: ToolRegistry.plugin(), // MCP tools
skill: ToolRegistry.skill() // User-defined skill tools
})File operations:
read — read file contents with offset/limitwrite — write/create filesedit — edit files with diff applicationapply_patch — apply git-style patchesSearch:
grep — search file contents (uses ripgrep)glob — find files by patternrepo_overview — get repository structureShell:
shell / bash — execute shell commandsrepo_clone — clone git repositoriesWeb:
websearch — search web (Exa or Parallel via MCP)webfetch — fetch URLs, convert to markdown/text/htmlAgent orchestration:
task — spawn sub-agent sessionstask_status — check sub-agent statusplan — enter/exit plan modeInteraction:
question — ask user structured questionstodo — read task liststodowrite — write todo itemsLanguage Server:
lsp — LSP operations (go-to-definition, diagnostics, etc.)Utilities:
external_directory — access directories outside projectskill — execute user-defined skillsinvalid — error handling for invalid tool callsTwo MCP-based providers:
| Provider | MCP URL | API Key |
|---|---|---|
| Exa | https://mcp.exa.ai/mcp | EXA_API_KEY |
| Parallel | https://search.parallel.ai/mcp | PARALLEL_API_KEY |
Selection: env var OPENCODE_WEBSEARCH_PROVIDER, flags, or hash-based random (session ID checksum % 2).
Default params: numResults: 8, livecrawl: fallback, timeout: 25s.
Technique: HTTP requests via Effect's HttpClient — no headless browser.
turndownhtmlparser2markdown, text, htmlNot included. No Puppeteer, Playwright, or any headless browser in core. Must be added via MCP server, custom tool, or skill plugin.
OpenCode uses Effect fibers — structured concurrency on top of Node.js's single-threaded event loop.
┌─────────────────────────────────────┐
│ Node.js Event Loop (Single Thread) │
│ ┌───────────────────────────────┐ │
│ │ Effect Fiber 1 (subagent A) │ │
│ │ Effect Fiber 2 (subagent B) │ │
│ │ Effect Fiber 3 (subagent C) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘| Feature | async/await | Effect Fibers |
|---|---|---|
| Scoped cleanup | ❌ manual | ✅ automatic |
| Interruption | ❌ limited | ✅ Fiber.interrupt() |
| Controlled concurrency | ❌ manual Promise.all | ✅ { concurrency: N } |
| Background jobs | ❌ none | ✅ background.start() |
| Structured hierarchy | ❌ none | ✅ scoped fibers |
// Unbounded parallel
yield* Effect.all([task1, task2, task3], { concurrency: "unbounded" })
// Controlled concurrency
yield* Effect.forEach(tasks, runTask, { concurrency: 8 })Does NOT consume more CPU cores. All fibers run on the single Node.js event loop. Operations are I/O bound (HTTP requests to LLM APIs, file reads) — not CPU bound.
CPU cores would only matter with worker threads or child processes — neither of which OpenCode uses.
| Operation | Concurrency |
|---|---|
| Tool calls | unbounded |
| File reads | 8 |
| Grep operations | 16 |
| Network fetches | 4 |
| Sequential operations | 1 |
src/background/job.ts)// Start background job
yield* background.start({ id: "task-123", type: "task", run: subagentTask() })
// Poll status
yield* background.wait({ id: "task-123" })
// Cancel
yield* background.cancel("task-123")Jobs stored in memory Map — not Redis, not a queue system.
OpenCode has no resource limits for sub-agents. No CPU quota, memory quota, network quota, or max concurrent sub-agents. Constraints come from outside:
| Source | Constraint |
|---|---|
| LLM provider | API rate limits |
| User | Approval prompts |
| Permission system | Tool access rules |
| System | Memory/network exhaustion |
OpenCode is a local developer tool, not a multi-tenant production system. It trusts the developer, provides permission controls, but enforces no resource quotas.
Creating a new chat (/new) does not cancel existing running sessions. Each session has its own independent runner. Multiple sessions can be "busy" simultaneously. Explicit cancel is required: POST /session/{sessionID}/abort.
yield* permission.ask({
permission: "bash",
patterns: ["*"],
metadata: { command: "rm -rf /" }
})Three outcomes:
once — allow this single callalways — allow forever (saved to SQLite)reject — deny + rejects all pending requests for the sessionPermission rules per agent:
{
bash: "ask", // Ask user each time
edit: "allow", // Always allow
task: "deny", // Never allow
read: {
"*.env": "ask", // Ask for .env files
"*": "allow" // Allow everything else
}
}Blocking mechanism: Effect-based deferred promises — tool execution halts until user responds. No race conditions.
yield* question.ask({
questions: [{
question: "Which framework should we use?",
options: [
{ label: "React", description: "Facebook's library" },
{ label: "Vue", description: "Progressive framework" }
],
multiple: false,
custom: true // Allow typing custom answer
}]
})LLM calls question tool → UI displays dialog → user responds → answers returned to LLM → LLM continues.
Solid:
always approvals persisted in SQLite (survive restarts)Not perfect:
Location: packages/opencode/src/session/
Created, Updated, Deleted, Diff, Errorbusy — LLM processingidle — waiting for inputsession/llm.ts)Per-request decision:
One SQLite file per project. No Redis, no external database, no distributed system.
Location: ~/.config/opencode/projects/{projectID}/db.sqlite
Key columns: id, project_id, parent_id (sub-agent hierarchy), title, agent, model, cost, tokens_*, permission (JSON), revert (JSON), timestamps.
Indexes: project_id, workspace_id, parent_id
Key columns: id, session_id, time_created, data (JSON — full MessageV2.Info)
Indexes: Composite (session_id, time_created, id) — optimised for timeline queries.
Key columns: id, message_id, session_id, data (JSON — MessageV2.Part)
Types of parts: text, tool_call, tool_result, reasoning, error.
Indexes: (message_id, id), session_id
PK: (session_id, position) — composite for ordering.
Columns: content, status, priority, position, timestamps.
Columns: id, worktree, vcs, name, icon_*, sandboxes (JSON), commands (JSON), timestamps.
PK: project_id. Stores full Ruleset as JSON. One row per project.
Columns: id, type, name, branch, directory, extra (JSON), project_id, time_used.
Stores OAuth tokens, refresh tokens, expiry, active account/org state.
Stores share URL, share ID, secret per session.
event_sequence: tracks aggregate sequence numbers.
event: stores typed events with aggregate_id, seq, type, data (JSON).
Session (chat metadata)
└─ Message (user/assistant turn)
└─ Part (text, tool_call, tool_result, reasoning, etc.)
Todo (task list, separate from parts)| Technique | Detail |
|---|---|
| JSON mode columns | text({ mode: "json" }) — automatic parsing, type-safe |
| Cascade deletes | Orphan cleanup on session/message delete |
| Integer timestamps | Unix ms — compact, fast comparisons |
| Text (ULID) PKs | No auto-increment — distributed-friendly |
| Composite indexes | On query-critical column combos |
| Foreign key constraints | Referential integrity enforced |
| Auto timestamps | $default and $onUpdate callbacks |
| Minimal schema | No redundant columns; complex data → JSON |
| Per-project DB | No multi-tenant overhead |
✅ Single-user local development
✅ Small-to-medium datasets (thousands of messages)
✅ Read-heavy (display chat history)
✅ Session-based queries
❌ Not for multi-user concurrent access
❌ Not for millions of records
❌ Not for complex analytical queries
Location: packages/opencode/src/auth/
Methods supported:
Storage: JSON files with 0o600 permissions (user-only read/write).
Provider-specific auth management — each LLM provider has its own auth handler.
Location: packages/opencode/src/server/
Features:
The server enables headless operation — the TUI, web, and desktop UIs all consume this API. The SDK (packages/sdk) is generated from OpenAPI spec.
testEffect helper)sleep for readiness — use Effect synchronisation insteadtest/AGENTS.md, test/EFFECT_TEST_MIGRATION.mdStrengths:
Weaknesses / things to be aware of:
| Feature | Status |
|---|---|
| Headless browser | ❌ Not included |
| Redis / external cache | ❌ Not used |
| Distributed locks | ❌ Not needed (single-process) |
| Task queue (BullMQ, etc.) | ❌ In-memory only |
| Resource quotas for sub-agents | ❌ Not implemented |
| Max sub-agent limit | ❌ No hard limit |
| Permission revocation UI | ❌ Must edit config manually |
| Approval timeout | ❌ Can hang forever |
| CPU/OS thread parallelism | ❌ Single-threaded event loop |
| Orchestration framework | ❌ Custom-built from scratch |
The core orchestration in OpenCode is domain-agnostic. The coding-specific parts are only:
Everything else — agent hierarchy, session management, permission evaluation, sub-agent spawning, tool registry, human-in-loop, Effect concurrency — works for any domain.
| Domain | Custom tools | Custom agents |
|---|---|---|
| Research assistant | websearch, paper_fetch, citation_extract, summary_generate | researcher, writer, analyst |
| Financial analyst | stock_query, portfolio_analyze, trade_execute, report_generate | analyst, trader, risk_assessor |
| Healthcare | patient_query, diagnosis_assist, treatment_plan | clinician, researcher, reviewer |
| Marketing | campaign_create, analytics_fetch, content_generate | strategist, copywriter, analyst |
@your-org/agent-harness package; OpenCode becomes one implementation