InkdownInkdown
Start writing

opencode-study

2 files·0 subfolders

Shared Workspace

opencode-study
context-handling

core

Shared from "opencode-study" on Inkdown

OpenCode — Deep Dive Knowledge Base

Reverse-engineered analysis of the OpenCode codebase: architecture, orchestration, tools, database, and design philosophy.


Table of Contents

  1. What Is OpenCode
  2. Folder Structure
  3. Design Architecture
  4. LLM Integration & Custom Orchestration Layer
  5. Agent System
  6. Sub-Agent Orchestration
  7. Tool System
core
  • Parallelism & Resource Allocation
  • Human-in-the-Loop System
  • Session Management
  • SQLite Database Design
  • Authentication System
  • Server Architecture
  • Build System
  • Testing Strategy
  • Security Considerations
  • What OpenCode Does NOT Have
  • Key Takeaways & Design Philosophy
  • Generalising the Harness Beyond Coding

  • 1. What Is OpenCode

    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:

    • Code understanding and codebase exploration
    • Writing, editing, and refactoring code
    • Automating development tasks via shell
    • Planning and architectural reasoning with sub-agents
    • Pair programming via terminal, web, or desktop UI

    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.


    2. Folder Structure

    Plain text
    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 files

    3. Design Architecture

    Pattern: Service-Oriented with Effect (functional programming)

    Every feature is a Service with three parts:

    PartPurpose
    InterfaceDefines what the service can do
    ImplementationHow it does it
    LayerHow to inject its dependencies

    Core services:

    ServiceResponsibility
    SessionManages AI conversations
    AgentAI personalities (build, plan, explore, etc.)
    ConfigLoads settings from files / env / remote
    AuthAPI keys and OAuth handling
    ProviderConnects to LLM providers (OpenAI, Anthropic, etc.)
    ToolFile ops, shell, web, orchestration
    ServerHTTP API for headless operation
    PermissionEvaluates and enforces tool access rules
    QuestionStructured human-in-loop prompts
    BackgroundManages async/background job execution

    Data flow:

    Plain text
    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:

    LayerTechnology
    LanguageTypeScript
    Functional coreEffect
    ValidationEffect Schema
    UISolidJS
    ORMDrizzle ORM
    DatabaseSQLite
    Runtime & buildBun

    4. LLM Integration & Custom Orchestration Layer

    Key insight: 100% custom-built — no LangChain, Vercel AI SDK, AutoGen, CrewAI, or any orchestration framework at the core.

    What they built themselves:

    • Agent management and hierarchy
    • Session creation and parent-child relationships
    • Permission derivation and composition
    • Sub-agent spawning via the task tool
    • Tool registry and execution orchestration
    • LLM route system (Protocol + Endpoint + Auth + Framing)
    • Session processor (handles LLM events, tool calls, results)

    What external libraries handle (infrastructure, not orchestration):

    LibraryRole
    EffectFP foundation (like React for UI — structural, not orchestration)
    Vercel AI SDKOptional provider transport adapter (one of two runtime options)
    DrizzleDatabase persistence
    Route-Based LLM Abstraction (packages/llm/src/route/)
    TypeScript
    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.

    Tool Runtime (packages/llm/src/tool-runtime.ts)
    • Custom tool execution engine
    • Manages tool-calling loops
    • Handles tool results and error handling
    • Integrates with the route system
    Tool Definition System
    TypeScript
    Tool.define(id, Effect.succeed({
      description: string,
      parameters: Schema,           // Effect Schema for validation
      execute: (args, ctx) => Effect.Effect<ExecuteResult>
    }))
    Session Orchestration (packages/opencode/src/session/llm.ts)
    • Decides between native route runtime vs Vercel AI SDK per request
    • Manages conversation state
    • Handles tool execution, feeding results back to LLM
    Tool Calling Flow
    Plain text
    LLM Request → Route.compile → Provider API
                       ↓
               Tool Call Event detected
                       ↓
               ToolRuntime.execute
                       ↓
               Tool.execute (Effect)
                       ↓
               Tool Result → Fed back to LLM

    5. Agent System

    Agent Structure

    Each agent has:

    TypeScript
    {
      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
    }
    Built-in Agent Types
    AgentModePurposeKey tools
    buildprimaryDefault — executes toolsall permitted tools
    planprimaryPlanning modeno edit tools
    generalprimaryMulti-step parallel tasksbroad tool set
    exploresubagentFast codebase explorationgrep, glob, read only
    scoutsubagentExternal docs & dependency research (experimental)webfetch, websearch
    compactionsubagentContext compactionread
    titlesubagentGenerate session titlesnone
    summarysubagentSummarise sessionsread
    Agent Selection

    User input → session processor → agent selected (based on config/request) → LLM called with agent's prompt + available tools


    6. Sub-Agent Orchestration

    How Sub-Agents Work

    Sub-agents are not picked from a queue. The parent agent explicitly spawns them by calling the task tool:

    TypeScript
    task({
      description: "Explore codebase",
      prompt: "Find all API endpoints",
      subagent_type: "explore"
    })

    This creates a new independent session:

    TypeScript
    sessions.create({
      parentID: ctx.sessionID,       // Link to parent
      title: description + " (@explore subagent)",
      permission: derivedPermissions // Merged from parent + subagent
    })
    Permission Derivation (Critical Safety Mechanism)

    src/agent/subagent-permissions.ts merges permissions in this order:

    Plain text
    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 set

    Sub-agents cannot have more permissions than their parent. This prevents permission escalation.

    Sub-Agent Lifecycle
    Plain text
    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)
    Execution Modes
    ModeBehaviour
    SynchronousParent waits for subagent to complete
    Background (background: true)Subagent runs async, parent continues immediately (experimental)
    ResumePass task_id to continue same subagent session
    Sub-Agent Limits

    No hard limit in code. Constrained by:

    • LLM provider rate limits (OpenAI, Anthropic API quotas)
    • Prompt instructions ("launch up to 3 explore agents in parallel")
    • User approval prompts (if tool requires ask)
    • System resources (memory, network — natural exhaustion)

    7. Tool System

    Hybrid approach: custom built-ins + MCP

    Tools are merged from three sources in the registry:

    TypeScript
    const tools = yield* Effect.all({
      builtIn: ToolRegistry.builtIn(),   // Custom tools
      plugin: ToolRegistry.plugin(),     // MCP tools
      skill: ToolRegistry.skill()        // User-defined skill tools
    })
    Core Built-in Tools (always available)

    File operations:

    • read — read file contents with offset/limit
    • write — write/create files
    • edit — edit files with diff application
    • apply_patch — apply git-style patches

    Search:

    • grep — search file contents (uses ripgrep)
    • glob — find files by pattern
    • repo_overview — get repository structure

    Shell:

    • shell / bash — execute shell commands
    • repo_clone — clone git repositories

    Web:

    • websearch — search web (Exa or Parallel via MCP)
    • webfetch — fetch URLs, convert to markdown/text/html

    Agent orchestration:

    • task — spawn sub-agent sessions
    • task_status — check sub-agent status
    • plan — enter/exit plan mode

    Interaction:

    • question — ask user structured questions
    • todo — read task lists
    • todowrite — write todo items

    Language Server:

    • lsp — LSP operations (go-to-definition, diagnostics, etc.)

    Utilities:

    • external_directory — access directories outside project
    • skill — execute user-defined skills
    • invalid — error handling for invalid tool calls
    Web Search

    Two MCP-based providers:

    ProviderMCP URLAPI Key
    Exahttps://mcp.exa.ai/mcpEXA_API_KEY
    Parallelhttps://search.parallel.ai/mcpPARALLEL_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.

    Web Fetch

    Technique: HTTP requests via Effect's HttpClient — no headless browser.

    • HTML → Markdown via turndown
    • HTML parsing via htmlparser2
    • Cloudflare bot detection retry (honest UA fallback)
    • Base64 image attachments
    • 5MB response size limit, 30s default / 120s max timeout
    • Output formats: markdown, text, html
    Headless Browser

    Not included. No Puppeteer, Playwright, or any headless browser in core. Must be added via MCP server, custom tool, or skill plugin.


    8. Parallelism & Resource Allocation

    Concurrency Model: Effect Fibers (not OS threads)

    OpenCode uses Effect fibers — structured concurrency on top of Node.js's single-threaded event loop.

    Plain text
    ┌─────────────────────────────────────┐
    │  Node.js Event Loop (Single Thread) │
    │  ┌───────────────────────────────┐  │
    │  │  Effect Fiber 1 (subagent A) │  │
    │  │  Effect Fiber 2 (subagent B) │  │
    │  │  Effect Fiber 3 (subagent C) │  │
    │  └───────────────────────────────┘  │
    └─────────────────────────────────────┘
    Fibers vs async/await
    Featureasync/awaitEffect 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
    TypeScript
    // Unbounded parallel
    yield* Effect.all([task1, task2, task3], { concurrency: "unbounded" })
    
    // Controlled concurrency
    yield* Effect.forEach(tasks, runTask, { concurrency: 8 })
    CPU Core Usage

    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.

    Concurrency Limits in Codebase
    OperationConcurrency
    Tool callsunbounded
    File reads8
    Grep operations16
    Network fetches4
    Sequential operations1
    Background Job System (src/background/job.ts)
    TypeScript
    // 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.

    Resource Allocation: None

    OpenCode has no resource limits for sub-agents. No CPU quota, memory quota, network quota, or max concurrent sub-agents. Constraints come from outside:

    SourceConstraint
    LLM providerAPI rate limits
    UserApproval prompts
    Permission systemTool access rules
    SystemMemory/network exhaustion
    What happens if you spawn 10 sub-agents doing heavy work?
    1. Permission check — tool not in agent's ruleset → denied
    2. User approval — 10 × N operations = N approval prompts
    3. LLM rate limits — provider throttles requests
    4. System crash — memory exhaustion with no graceful handling

    OpenCode is a local developer tool, not a multi-tenant production system. It trusts the developer, provides permission controls, but enforces no resource quotas.

    Session Independence

    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.


    9. Human-in-the-Loop System

    Two separate systems
    1. Permission System (Tool Approval)
    TypeScript
    yield* permission.ask({
      permission: "bash",
      patterns: ["*"],
      metadata: { command: "rm -rf /" }
    })

    Three outcomes:

    • once — allow this single call
    • always — allow forever (saved to SQLite)
    • reject — deny + rejects all pending requests for the session

    Permission rules per agent:

    TypeScript
    {
      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.

    2. Question System (Structured Input)
    TypeScript
    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.

    Reliability Assessment: 7/10

    Solid:

    • Blocking via Effect deferred is reliable (no bypass, no race condition)
    • always approvals persisted in SQLite (survive restarts)
    • Session rejection cascades (reject one = reject all pending in session)

    Not perfect:

    • No timeout — if user walks away, request hangs forever
    • No permission revocation UI — must edit config file manually
    • No "deny forever" for specific patterns
    • Sub-agent permission inheritance logic is complex
    • No rate limiting on permission request spam

    10. Session Management

    Location: packages/opencode/src/session/

    Features
    • Multi-turn conversation management
    • Message and part storage (hierarchical)
    • Token usage tracking (input, output, reasoning, cache read/write)
    • Cost calculation
    • Session forking
    • Event-driven: Created, Updated, Deleted, Diff, Error
    • Parent-child session hierarchy (sub-agents)
    Session States
    • busy — LLM processing
    • idle — waiting for input
    Runtime Selection (session/llm.ts)

    Per-request decision:

    • Native route runtime (opt-in) — custom Effect-based system
    • AI SDK runtime (default) — Vercel AI SDK as provider adapter

    11. SQLite Database Design

    One SQLite file per project. No Redis, no external database, no distributed system.

    Location: ~/.config/opencode/projects/{projectID}/db.sqlite

    Tables Overview
    Session Table (chats)

    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

    Message Table

    Key columns: id, session_id, time_created, data (JSON — full MessageV2.Info)

    Indexes: Composite (session_id, time_created, id) — optimised for timeline queries.

    Part Table (message components)

    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

    Todo Table

    PK: (session_id, position) — composite for ordering. Columns: content, status, priority, position, timestamps.

    Project Table

    Columns: id, worktree, vcs, name, icon_*, sandboxes (JSON), commands (JSON), timestamps.

    Permission Table

    PK: project_id. Stores full Ruleset as JSON. One row per project.

    Workspace Table

    Columns: id, type, name, branch, directory, extra (JSON), project_id, time_used.

    Account / Account State Tables

    Stores OAuth tokens, refresh tokens, expiry, active account/org state.

    Session Share Table

    Stores share URL, share ID, secret per session.

    Event Tables (Event Sourcing)

    event_sequence: tracks aggregate sequence numbers. event: stores typed events with aggregate_id, seq, type, data (JSON).

    Chat Storage Hierarchy
    Plain text
    Session (chat metadata)
      └─ Message (user/assistant turn)
          └─ Part (text, tool_call, tool_result, reasoning, etc.)
             Todo (task list, separate from parts)
    Optimisations Used
    TechniqueDetail
    JSON mode columnstext({ mode: "json" }) — automatic parsing, type-safe
    Cascade deletesOrphan cleanup on session/message delete
    Integer timestampsUnix ms — compact, fast comparisons
    Text (ULID) PKsNo auto-increment — distributed-friendly
    Composite indexesOn query-critical column combos
    Foreign key constraintsReferential integrity enforced
    Auto timestamps$default and $onUpdate callbacks
    Minimal schemaNo redundant columns; complex data → JSON
    Per-project DBNo multi-tenant overhead
    What the DB is optimised for

    ✅ 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


    12. Authentication System

    Location: packages/opencode/src/auth/

    Methods supported:

    • OAuth (browser-based)
    • API key
    • Well-known auth (auto-discovery)

    Storage: JSON files with 0o600 permissions (user-only read/write).

    Provider-specific auth management — each LLM provider has its own auth handler.


    13. Server Architecture

    Location: packages/opencode/src/server/

    Features:

    • HTTP API with OpenAPI spec
    • WebSocket for real-time updates
    • mDNS for local network discovery
    • CORS handling (configurable origins)
    • Graceful shutdown

    The server enables headless operation — the TUI, web, and desktop UIs all consume this API. The SDK (packages/sdk) is generated from OpenAPI spec.


    14. Build System

    • Tool: Bun build
    • Targets: 12 platform/architecture combinations (Linux/macOS/Windows × arm64/x64 × glibc/musl)
    • Features: Cross-platform binary generation, embedded Web UI bundling, migration embedding, source maps (optional), smoke testing for current platform, release automation with GitHub uploads

    15. Testing Strategy

    • Framework: Bun test
    • Test types: Unit, integration, CLI, HTTP API, Effect-specific (testEffect helper)
    • Fixtures: Temporary directories, test providers, mock services
    • Snapshot testing: For stable outputs
    • Anti-patterns documented: Avoid sleep for readiness — use Effect synchronisation instead
    • 15+ test directories covering all major systems
    • Test guides: test/AGENTS.md, test/EFFECT_TEST_MIGRATION.md

    16. Security Considerations

    Strengths:

    • Multiple auth methods, secure storage (0o600)
    • Default-deny permission system for tools
    • Per-agent permission isolation
    • Session-level rejection cascades
    • Schema-based input validation throughout

    Weaknesses / things to be aware of:

    • API keys stored in JSON files (albeit with restricted permissions)
    • No sub-agent count or resource limits
    • No permission revocation UI
    • No timeout on permission approval prompts
    • Complex sub-agent permission inheritance can create accidental loopholes

    17. What OpenCode Does NOT Have

    FeatureStatus
    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

    18. Key Takeaways & Design Philosophy

    1. Local-first, single-user — no multi-tenancy, no cloud dependency for core logic
    2. Trust the developer — no resource guards; constraints come from permissions, user approvals, and external APIs
    3. Service-oriented with Effect — every feature is a service with interface + implementation + layer; Effect provides typed error handling, structured concurrency, and dependency injection
    4. Protocol-agnostic LLM routing — one route protocol works across providers; provider-specific adapters plug in
    5. Tools are the extension point — core orchestration is generic; coding-specific behaviour lives in tool implementations, agent configs, and prompts
    6. MCP for external extensibility — custom tools for core, MCP for everything else
    7. Sessions are the unit of isolation — each conversation (including sub-agents) is an independent session with its own runner, permissions, and message history
    8. Permission inheritance is the safety guarantee — sub-agents can never exceed parent permissions; default-deny is the base posture

    19. Generalising the Harness Beyond Coding

    The core orchestration in OpenCode is domain-agnostic. The coding-specific parts are only:

    • Tool implementations
    • Agent configurations
    • Prompts / instructions
    • Permission rule names

    Everything else — agent hierarchy, session management, permission evaluation, sub-agent spawning, tool registry, human-in-loop, Effect concurrency — works for any domain.

    Domain Mapping Examples
    DomainCustom toolsCustom agents
    Research assistantwebsearch, paper_fetch, citation_extract, summary_generateresearcher, writer, analyst
    Financial analyststock_query, portfolio_analyze, trade_execute, report_generateanalyst, trader, risk_assessor
    Healthcarepatient_query, diagnosis_assist, treatment_planclinician, researcher, reviewer
    Marketingcampaign_create, analytics_fetch, content_generatestrategist, copywriter, analyst
    Three Approaches to Generalise
    1. Fork OpenCode — keep core, replace tools/agents/prompts, minimal changes
    2. Extract core library — create @your-org/agent-harness package; OpenCode becomes one implementation
    3. Template system — OpenCode as scaffolding template, domain-specific configs generated per use case
    What reuses directly (zero changes needed)
    • Agent orchestration (task spawning, hierarchy)
    • Session management (conversation state)
    • Permission evaluation system
    • Effect-based concurrency
    • Tool registry (add new tools, remove old ones)
    • Human-in-loop (permission + question systems)
    • MCP integration
    • Background job system
    • HTTP server and SDK generation