Skip to content

Instantly share code, notes, and snippets.

@shibuiwilliam
Created February 22, 2026 07:54
Show Gist options
  • Select an option

  • Save shibuiwilliam/1d1466b24cb5c8f0d9367f2c75c9c064 to your computer and use it in GitHub Desktop.

Select an option

Save shibuiwilliam/1d1466b24cb5c8f0d9367f2c75c9c064 to your computer and use it in GitHub Desktop.
Dissecting OpenCode — 10 Design Elements Every Coding Agent Developer Should Know

Dissecting OpenCode — 10 Design Elements Every Coding Agent Developer Should Know

A deep dive into the architecture, agent loop, tool system, and more behind the 100k-star open-source AI coding agent

As AI coding agents rapidly evolve, open-source projects that let you study their internals are invaluable. OpenCode is an open-source AI coding agent with over 100,000 GitHub stars. It supports multiple client surfaces — terminal (TUI), web browser, desktop app, and IDE extensions — and works with 75+ LLM providers out of the box.

In this article, we take a deep dive into OpenCode's documentation and source code to identify 10 critical technical elements that engineers building their own coding agents should understand. Rather than a feature overview, we focus on the "why" behind each design decision and how you can apply these patterns to your own agent.

By the end of this article, you will understand:

  • The core architecture of a coding agent and the key design trade-offs
  • Concrete implementation patterns for the agent loop (LLM call → tool execution → response recording)
  • Design approaches for tool systems, permission control, and provider abstraction
  • Integration strategies for TUI, LSP, MCP, and plugin systems

Prerequisites: Familiarity with LLM fundamentals (prompts, tokens, streaming), basic TypeScript knowledge, and terminal experience.


1. Overall Architecture: Client-Server Separation

Why Separate Client and Server?

When building a coding agent, the first design decision you face is how to separate the UI from the core logic. OpenCode adopts a client-server architecture. On startup, it launches an HTTP server, and the TUI operates as a client connecting to that server.

This design has clear advantages. Here's the high-level picture:

┌─────────────────────── Clients ───────────────────────┐
│  TUI (Terminal)                                       │
│  Web Browser                                          │
│  Desktop App (Tauri)                                  │
│  IDE Extension (VS Code)                              │
│  SDK (TypeScript)                                     │
└──────────────┬────────────────────────┬───────────────┘
               │ REST API              │ SSE
               ▼                       ▼
┌─────────────────── HTTP Server (Hono) ────────────────┐
│  REST API (OpenAPI 3.1)                               │
│  SSE Endpoint (/global/event)                         │
│  Agent Engine                                         │
│  SQLite (Drizzle ORM) ── per-project database         │
└───────────────────────────────────────────────────────┘

Key design points:

  • Multi-client support — Multiple clients can connect to a single server simultaneously. You can launch headlessly with opencode serve and attach from a web UI or another terminal.
  • Real-time synchronization — All clients receive the same streaming output in real time through the Server-Sent Events (SSE) endpoint at /global/event.
  • API-first design — A REST API conforming to the OpenAPI 3.1 specification is exposed at /doc, enabling third-party developers to build their own clients.

Per-Project Isolation

OpenCode maintains an independent SQLite database for each project at ~/.local/share/opencode/project/<hash>/data.db. The HTTP server's Instance.provide() middleware scopes each request to its project directory, ensuring sessions and permissions never bleed across projects.

Takeaways for Coding Agent Developers

Even for your own agent, separate the UI from the agent logic early on. You might start with just a CLI, but you will inevitably want to add a web UI or IDE extension later. The HTTP API + SSE pattern offers low implementation cost and high extensibility.


2. The Agent Loop: The Heart of a Coding Agent

Loop Overview

The most critical component of a coding agent is the agent loop. In OpenCode, this is implemented as SessionPrompt.loop(). It receives user input, calls the LLM, executes tools, records results — and repeats this cycle. This is the essence of what makes an agent an agent.

User Input
    │
    ▼
[1] Create User Message (attach file context & metadata)
    │
    ▼
[2] Resolve Tool Registry (build AI SDK tool definitions)
    │
    ▼
[3] Insert Reminders (inject system reminders for queued messages)
    │
    ▼
[4] LLM Streaming (call model via SessionProcessor)
    │
    ▼
[5] Tool Execution (permission check → plugin hooks → execute)
    │
    ▼
[6] Record Response (store assistant message in DB)
    │
    ▼
    finish_reason == "tool-calls"? ──Yes──► Go to step [2]
         │
         No
         │
         ▼
    Return Final Response

Message Processing Pipeline

Before calling the LLM, a pipeline transforms internal messages into the API format. SessionProcessor.create() executes the following transformations in sequence:

  1. MessageV2.toModelMessages() — Converts internal message structures to AI SDK format, transforming polymorphic Parts into a unified format.
  2. ProviderTransform.message() — Applies provider-specific transformations. For example, Anthropic requires filtering empty content, while Mistral needs tool IDs normalized to 9 alphanumeric characters.
  3. ProviderTransform.options() — Sets model parameters such as maxTokens, temperature, and topP.
  4. ProviderTransform.applyCaching() — Adds prompt cache control headers for Anthropic, Bedrock, and OpenRouter.
  5. LLM.stream() — Executes the actual streaming call with error recovery and retry logic.

The Polymorphic Part System

OpenCode messages are composed of arrays of typed Part objects. This design is key to the extensibility of a coding agent.

// Part types (conceptual code)
type Part =
  | { type: "text"; content: string }
  | { type: "tool-invocation"; toolName: string; args: Record<string, unknown>; result: ToolResult }
  | { type: "reasoning"; content: string }
  | { type: "file"; path: string; content: string }
  | { type: "image"; data: Buffer; mimeType: string }
  | { type: "agent"; agentName: string; sessionID: string }

Each Part has its own schema and rendering logic. Adding new content types (e.g., audio, video) does not affect existing Parts.

Two-Tier Agent Architecture

OpenCode employs a two-tier system of primary agents and subagents:

  • Build (Primary) — Full tool access. The default development agent.
  • Plan (Primary) — Read-only; write, edit, and bash are disabled. Used for safe code analysis and planning.
  • General (Subagent) — Full access except todo. Delegation target for complex searches.
  • Explore (Subagent) — Read-only. Designed for codebase exploration.
  • Compaction (Hidden) — Handles context compression.
  • Title (Hidden) — Generates session names.

Primary agents are switched with the Tab key, while subagents are automatically invoked via the task tool. Using the Plan agent during the planning phase and the Build agent during implementation prevents unintended file modifications.

Takeaways for Coding Agent Developers

Three key principles for agent loop design:

  1. Always feed tool execution results back to the next LLM call. If finish_reason is tool-calls, continue the loop.
  2. Create a transformation layer to absorb provider-specific differences. Isolate provider quirks (empty content handling, ID format requirements, etc.) from the loop body.
  3. Manage messages as arrays of structured Parts. Separating text, tool invocations, and reasoning traces makes rendering and persistence far more flexible.

3. Tool System and Permission Control

14 Built-in Tools

Tools are the "hands and feet" of a coding agent. OpenCode ships with 14 built-in tools, organized into five categories:

File Operationsread (includes LSP diagnostics in output), write (full file replacement), edit (search/replace-based diff editing), patch (batch changes via patch file format)

Searchgrep (uses ripgrep internally, respects .gitignore), glob (sorted by modification time), list (tree-style directory listing)

Executionbash (runs in pseudo-terminal), task (spawns child agents for subagent delegation)

Knowledgeskill (on-demand context injection), webfetch (page fetching and parsing), websearch (via Tavily API), lsp (type info, symbols, diagnostics)

Interactionquestion (blocks waiting for user response)

Tool Definition Pattern

Tools are defined using Tool.define() with Zod schemas for argument validation.

// Conceptual code for tool definition
import { z } from "zod"

const readTool = Tool.define({
  name: "read",
  description: "Read the contents of a file",
  inputSchema: z.object({
    file_path: z.string().describe("Path to the file to read"),
    offset: z.number().optional().describe("Starting line number"),
    limit: z.number().optional().describe("Number of lines to read"),
  }),
  async execute(args, ctx) {
    // Read file + attach LSP diagnostics
    const content = await readFile(args.file_path, args.offset, args.limit)
    const diagnostics = await getLspDiagnostics(args.file_path)
    return { content, diagnostics }
  },
})

Tool.Context includes the session ID, message ID, agent name, abort signal, and a permission confirmation function. This gives complete visibility into the execution context.

Tool Execution Pipeline

Tool invocations pass through a 6-stage pipeline:

LLM generates tool call
  → Registry lookup
  → Permission check (allow / deny / ask)
  → Plugin pre-hook (tool.execute.before)
  → Tool execution (execute())
  → Plugin post-hook (tool.execute.after)
  → DB commit + SSE event emission

Tool Parts track state transitions: pendingexecutingcompleted | error. Each transition is streamed to clients via SSE, allowing users to monitor tool execution status in real time.

Permission Control

For a coding agent, what to allow and what to deny is the cornerstone of safety. OpenCode's permission system consists of three actions (allow, deny, ask) and glob pattern-based rule matching.

{
  "permission": {
    "read": { ".env": "deny", "**": "allow" },
    "bash": { "rm -rf *": "deny", "git *": "allow", "**": "ask" },
    "edit": { "**": "allow" },
    "external_directory": { "**": "ask" }
  }
}

Evaluation priority follows this hierarchy:

  1. Session-level permissions (highest priority)
  2. Agent-level permissions
  3. Global configuration permissions (lowest priority)

The last matching glob pattern is applied. Approved permissions are persisted in the database, so users don't need to re-approve the same operation repeatedly.

Custom Tools

Adding custom tools is as simple as placing a TypeScript file in the .opencode/tools/ directory. The filename becomes the tool name.

// .opencode/tools/deploy.ts
import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Deploy the application to the staging environment",
  args: {
    environment: tool.schema.string().describe("Target environment name"),
    version: tool.schema.string().optional().describe("Version to deploy"),
  },
  async execute(args) {
    // Deployment logic
    return `Deployment to ${args.environment} completed successfully`
  },
})

Custom tools can override built-in tools with the same name. This design lets teams embed their specific workflows directly into the agent.


4. Provider Abstraction Layer

Handling 75+ Providers Uniformly

When building a coding agent, locking into a single LLM provider is not realistic. OpenCode introduces a provider abstraction layer that handles 75+ LLM providers through a unified interface.

Provider Resolution Chain

Model selection follows this priority order:

  1. CLI flag (--model / -m) — highest priority
  2. Config filemodel field in opencode.json
  3. Previously used model — restored from the KV store
  4. Internal priority — first model by default ranking

Credential resolution is also hierarchical:

  1. Environment variables (highest) — ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
  2. Auth file~/.local/share/opencode/auth.json
  3. Config fileprovider section in opencode.json
  4. Models.dev database — default settings

ProviderTransform: Absorbing Provider Quirks

The ProviderTransform namespace, which absorbs per-provider differences, is a design pattern well worth studying.

// Conceptual code: provider-specific transformations
namespace ProviderTransform {
  // Anthropic: remove empty content (prevents API errors)
  // Mistral: normalize tool call IDs to 9 alphanumeric chars
  // OpenAI: no transformation needed
  function message(provider: string, messages: Message[]): Message[] {
    switch (provider) {
      case "anthropic":
        return messages.filter((m) => m.content.length > 0)
      case "mistral":
        return messages.map((m) => normalizeToolIds(m, 9))
      default:
        return messages
    }
  }

  // Anthropic, Bedrock, OpenRouter: add prompt cache control headers
  function applyCaching(provider: string, messages: Message[]): Message[] {
    if (["anthropic", "bedrock", "openrouter"].includes(provider)) {
      return addCacheControlHeaders(messages)
    }
    return messages
  }
}

The key takeaway here is completely isolating provider differences from the agent loop. The loop body only deals with a simple interface: send messages, receive responses.

Authentication Diversity

OpenCode supports four authentication methods:

  • API keys — OpenAI, Anthropic, Google, DeepSeek
  • OAuth flows — GitHub Copilot, GitLab Duo, Anthropic Claude Pro/Max
  • Cloud credentials — Amazon Bedrock (AWS), Google Vertex AI
  • Local endpoints (no auth) — Ollama, LM Studio, vLLM

All provider SDKs are bundled — no additional npm installations required. Users simply specify model: "anthropic/claude-sonnet-4-20250514" in the provider_id/model_id format to switch models.


5. OpenTUI: A Terminal UI Built with Zig and SolidJS

Why Build a Custom TUI Framework?

One of OpenCode's most distinctive technical decisions is the development of OpenTUI, a custom TUI framework. Rather than using existing libraries (Ink for React, Bubbletea for Go, etc.), they built a framework from scratch.

The reason: to achieve 60 FPS terminal rendering. Displaying LLM streaming output in real time while simultaneously handling scrolling, syntax highlighting, and diff views required performance that existing frameworks couldn't deliver.

The Zig + TypeScript Dual-Layer Architecture

OpenTUI has a dual-layer structure combining Zig and TypeScript:

┌──────────── TypeScript Layer ────────────┐
│  SolidJS Reconciler (declarative UI)     │
│  Yoga Layout Engine (Flexbox)            │
│  Component Catalogue (Box, Text, Code…)  │
└──────────────────┬───────────────────────┘
                   │
          Bun dlopen() FFI
                   │
                   ▼
┌──────────── Zig Layer (native) ──────────┐
│  Frame Diffing (cell array comparison)   │
│  ANSI Escape Generation (RLE encoding)   │
│  Rope-based Text Buffers                 │
│  Terminal Capability Detection           │
└──────────────────┬───────────────────────┘
                   │
                   ▼
            Terminal Output (stdout)

The Zig core handles the most performance-critical operations:

  • Frame diffing — Compares the current and previous frame cell arrays to identify only the changed portions
  • ANSI generation — Run-length encodes cells with identical styles to minimize output bytes
  • Rope buffers — Handles text insertions and deletions in O(log n)

The TypeScript bindings use Bun's dlopen() FFI to load Zig binaries. Six platform variants are supported (macOS/Linux/Windows × arm64/x64).

SolidJS Reconciler

OpenTUI uses createRenderer from solid-js/universal to connect SolidJS reactivity to terminal rendering.

SolidJS compile-time JSX transformation generates direct calls to createElement, insertNode, and setProperty. There is no virtual DOM. A componentCatalogue maps JSX tag names to Renderable constructors.

The key Renderable components are:

  • BoxRenderable — Layout containers with borders, backgrounds, and titles
  • TextRenderable — Styled text display
  • EditBufferRenderable — Multi-line editor with cursor and undo/redo support
  • CodeRenderable — Syntax highlighting via Tree-sitter grammars
  • DiffRenderable — Unified/split diff views
  • ScrollBoxRenderable — Scrollable regions with custom acceleration

Rendering Pipeline

A single frame is processed through the following pipeline:

Request → Yoga layout → Component render(buffer) → Hit grid mapping
  → Zig frame diff → Zig ANSI generation → Terminal output → Buffer swap

This achieves sub-millisecond frame times, maintaining smooth scrolling and display updates even during streaming output.

Takeaways for Coding Agent Developers

You don't need to build your own TUI framework, but the following design principles are worth noting:

  • Write only performance-critical parts in a native language. OpenCode writes the bulk of its code in TypeScript, delegating only frame diffing and ANSI generation to Zig.
  • Leverage declarative UI frameworks. SolidJS reactivity eliminates the need for manual screen updates on state changes.
  • Embrace modern terminal protocols. Kitty keyboard protocol, OSC 11 (background color detection), OSC 52 (clipboard integration) — modern terminals offer capabilities worth exploiting.

6. LSP Integration for Code Intelligence

Why Does an Agent Need LSP?

Reading files alone is not enough for a coding agent to grasp the full picture. "Where is this function called from?" "What type is this variable?" "Are there errors in this file?" — answering these questions accurately requires the Language Server Protocol (LSP).

OpenCode ships with 30+ pre-configured language servers. When a file extension is detected, the corresponding language server starts automatically.

Capabilities Exposed by the LSP Tool

The lsp tool (experimental) exposes the following LSP operations to the agent:

  • goToDefinition — Locate function or class implementations
  • findReferences — Understand the impact scope of changes
  • hover — Retrieve type information and documentation
  • documentSymbol — Understand file structure
  • workspaceSymbol — Search for symbols across the entire project
  • goToImplementation — Locate interface implementations
  • callHierarchy — Trace callers and callees of functions

Integration with the Read Tool

Notably, the read tool includes LSP diagnostic information in its output. When the agent reads a file, warnings, errors, and hints for that file are automatically appended. This means the agent can understand both the file's content and its issues in a single tool invocation.

Configuration Example

{
  "lsp": {
    "typescript-language-server": {
      "command": ["typescript-language-server", "--stdio"],
      "extensions": [".ts", ".tsx", ".js", ".jsx"]
    },
    "rust-analyzer": {
      "disabled": false,
      "extensions": [".rs"]
    },
    "custom-lsp": {
      "command": ["custom-lsp-server", "--stdio"],
      "extensions": [".custom"],
      "env": { "CUSTOM_VAR": "value" },
      "initialization": { "settings": {} }
    }
  }
}

Takeaways for Coding Agent Developers

LSP integration dramatically improves coding agent accuracy.

  • Attach diagnostics when reading files. The agent "naturally" becomes aware of code issues.
  • Combine go-to-definition with find-references. The agent's efficiency in navigating codebases improves dramatically.
  • Auto-start language servers. Deliver code intelligence appropriate to the project without any configuration burden on the user.

7. MCP Integration for External Tool Extension

What Is MCP?

Model Context Protocol (MCP) is a standard protocol for providing external tools to LLMs. OpenCode uses @modelcontextprotocol/sdk as its MCP client, seamlessly integrating external server tools into the agent.

Local and Remote Servers

MCP servers come in two types.

{
  "mcp": {
    "local-server": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem"],
      "environment": { "ROOT_DIR": "/path/to/dir" },
      "timeout": 10000
    },
    "remote-server": {
      "type": "remote",
      "url": "https://mcp.example.com/sse",
      "headers": { "Authorization": "Bearer {env:MCP_TOKEN}" },
      "timeout": 5000
    }
  }
}
  • Local servers — Executed as CLI commands. Specify the startup command via the command array and pass environment variables through environment.
  • Remote servers — Connect via HTTP. OAuth authentication is also supported — upon detecting a 401 response, Dynamic Client Registration (RFC 7591) is automatically initiated.

Integration into the Tool Registry

When an MCP server connects, its tools are automatically registered in the same ToolRegistry as built-in tools. From the LLM's perspective, built-in tools and MCP tools are indistinguishable.

The same allow / deny / ask permission framework applies, so external tools can be added without compromising security.

Takeaways for Coding Agent Developers

MCP support dramatically enhances an agent's ecosystem extensibility.

  • Manage built-in and external tools in a unified registry. Consistency from the LLM's perspective is maintained.
  • Apply permission control to external tools. Control "auto-execute OK" vs. "requires user confirmation" on a per-tool basis.
  • Support OAuth authentication. Integration with SaaS tools becomes significantly easier.

8. Context Window Management and Compaction

Why Context Management Matters

Coding agent conversations tend to grow long. File reads, tool execution results, and code diffs accumulate rapidly, quickly exhausting the LLM's context window. OpenCode solves this problem with a compaction system.

Three-Stage Context Management

The compaction system, implemented in src/session/compaction.ts, operates in three stages:

Monitor token count
    │
    ▼
Total tokens > context window?
    │              │
   No              Yes
    │              │
    ▼              ▼
  (wait)     [1] Pruning ── compress old tool outputs
                   │
                   ▼
             [2] Compaction ── summarize entire conversation
                   │
                   ▼
             [3] Replace with summary message
                   │
                   ▼
             (resume monitoring)

Stage 1: Pruning — The system scans message history from the end, protecting the most recent 40,000 tokens (PRUNE_PROTECT). Only tool outputs older than this window that exceed 20,000 tokens (PRUNE_MINIMUM) are removed.

These two parameters — the "protection window" and the "minimum pruning threshold" — are critical:

  • If the protection window is too small, recent context is lost, and the agent starts repeating the same operations.
  • If the minimum threshold is too small, frequent pruning causes thrashing.

Output from the skill tool is protected from pruning. Skills contain project-specific rules and procedures, and losing them mid-conversation degrades quality.

Stage 2: Compaction — When pruning alone isn't sufficient, the hidden Compaction agent summarizes the entire conversation. The summary includes the conversation's goal, user instructions, discovered facts, completed work, and a list of relevant files.

Stage 3: Replacement — The summary message is inserted as a new assistant message, replacing the old message history.

Takeaways for Coding Agent Developers

Context management directly impacts reliability during long coding sessions.

  • Always protect recent context. If the latest tool results or user instructions are lost, the agent will flounder.
  • Apply pruning and compaction progressively. First prune tool outputs; if that's insufficient, compact the entire conversation.
  • Protect specific tool outputs from pruning. Project rules and skill information must never disappear from context.

9. Session Management and the Database.effect Pattern

Session Branching

OpenCode sessions support parent-child relationships. Session.fork() copies conversation history up to a specified message, creating a child session with its own ID while maintaining lineage through parentID. The original session is unaffected.

This feature enables exploratory coding: "If this approach doesn't work, I can go back to this point and try again."

The Database.effect Pattern

The most noteworthy architectural pattern in OpenCode's database layer is Database.effect.

A common problem in real-time systems is the race condition where events are emitted before the database write commits, causing clients to observe inconsistent state. OpenCode solves this with the Database.effect pattern.

// Conceptual code: Database.effect pattern
Database.use(async (tx) => {
  // Update database within a transaction
  await tx.insert(messages).values({
    sessionID,
    role: "assistant",
    content: responseContent,
  })

  // Schedule event emission for after transaction commit
  Database.effect((effect) => {
    if (effect.changes.messages) {
      EventBus.publish("message.created", {
        sessionID,
        messageID: newMessage.id,
      })
    }
  })
})
// ← Transaction commits here,
//    THEN events inside the effect are emitted

How it works:

  1. All database mutations execute within a Database.use() transaction.
  2. Database.effect() schedules event emission.
  3. Events are emitted only after the transaction commits successfully.

This design eliminates the scenario where a client receives an event but can't find the corresponding data.

Database Schema

OpenCode uses a per-project SQLite database with five tables:

  • sessions — Session metadata with CASCADE delete
  • messages — Message bodies with session foreign keys
  • parts — Typed message parts with dual indexes
  • permissions — Tool execution grants, scoped per-session
  • mcp_servers — MCP connection state

The ORM is Drizzle ORM. SQLite was chosen because per-project isolation is straightforward and no external database server is required.

Takeaways for Coding Agent Developers

  • Emit events only after the database commit. Prematurely emitting events in pursuit of real-time responsiveness causes clients to observe inconsistent state.
  • Support session branching. Coding work is inherently exploratory, making "go back to this point and retry" functionality extremely valuable in practice.
  • Use SQLite for per-project isolation. No server needed, highly portable, and zero data interference between projects.

10. Plugin System and Event-Driven Architecture

Plugin Loading

OpenCode plugins are loaded from three sources:

  1. npm packages — Specified in config and auto-installed by Bun. Cached at ~/.cache/opencode/node_modules/.
  2. Local files.ts / .js files placed in .opencode/plugins/ or ~/.config/opencode/plugins/.
  3. Default plugins — Auto-loaded unless explicitly disabled.

20+ Hook Points

Plugins operate in an event-driven manner. Here are some of the key hook points:

  • chat.params — Fires when setting LLM call parameters. Use for dynamic adjustment of temperature and maxTokens.
  • chat.messages.transform — Fires when transforming message history. Use for custom prompt injection.
  • llm.stream.before / after — Fires before/after LLM streaming. Use for latency measurement and custom logging.
  • tool.execute.before / after — Fires before/after tool execution. Use for execution interception and result modification.
  • message.updated — Fires when a message is updated. Use for external notifications (e.g., Slack integration).
  • session.compaction — Fires during compaction. Use for custom summarization logic.
  • shell.env — Fires when setting shell environment variables. Use for dynamic environment variable injection.

Hooks execute using a pipeline pattern. Each plugin receives the previous plugin's output, transforms it, and passes it to the next.

Plugin Context

Plugins have access to rich context information:

// Context received by plugins (conceptual code)
interface PluginContext {
  project: {
    id: string
    path: string
  }
  directory: string
  worktree: string
  client: OpencodeClient  // SDK client instance
  $: ShellAPI             // Shell command execution API
}

The $ shell API enables plugins to safely execute shell commands. Through client, plugins can access the full OpenCode API.

Takeaways for Coding Agent Developers

Plugin system design determines the extensibility of your agent.

  • Build event-driven hook points into the design from the start. Retrofitting hooks into existing code requires extensive modifications.
  • Chain hooks using the pipeline pattern. Multiple plugins can process the same event in sequence.
  • Provide rich context information. Giving plugins access to project information and shell APIs enables a wide range of customizations.

Conclusion

In this article, we dissected OpenCode's source code and documentation to identify 10 technical elements that every coding agent developer should understand. Here's a recap:

  1. Client-server separation — Multi-client support via HTTP API + SSE
  2. Agent loop — LLM → tool execution → response cycle controlled by finish_reason
  3. Tool system — Zod schema definitions + 6-stage pipeline + permission control
  4. Provider abstractionProviderTransform isolates provider quirks
  5. OpenTUI — Zig for rendering performance, SolidJS for declarative UI
  6. LSP integration — Auto-attach diagnostics to the read tool
  7. MCP integration — Manage external tools in the same registry as built-ins
  8. Context management — Protection window + progressive pruning + compaction
  9. Database.effect — Event emission only after transaction commit
  10. Plugin system — 20+ hook points using the pipeline pattern

Advice for Coding Agent Developers

The most important lesson from OpenCode is to rigorously apply separation of concerns:

  • Separate the UI from agent logic (client-server)
  • Separate provider differences from the loop (ProviderTransform)
  • Separate performance-critical parts (Zig core)
  • Separate extension logic from the core (plugins + MCP)

It is precisely because these separations are done well that OpenCode can simultaneously support 75+ providers, multiple clients, and numerous plugins while maintaining codebase coherence.

When designing your own coding agent, start with a minimal setup — agent loop + a few tools + one LLM provider — and progressively introduce the separation patterns described in this article.


If you found this article useful, feel free to give it a clap and share it with fellow engineers. For more details, check out the resources below.

References:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment