Chapter 1

What is OpenCode?

📖 5 min read 🌱 Beginner friendly

OpenCode is an open-source AI coding agent that runs in the terminal. It reads your codebase, writes code, runs shell commands, searches the web, and reasons about problems. You pick the AI provider and model. You run it locally with your own API keys.

It is MIT licensed. The code is at github.com/anomalyco/opencode.

History

OpenCode started as a Go project built with Bubble Tea by Kujtim Hoxha. Dax Raad and the team at Anomaly (formerly Charm/Crush) rewrote it in TypeScript. The rewrite replaced the original architecture with a client-server model, event-sourcing persistence, a plugin system, and support for multiple frontends.

TypeScript was chosen for access to the Vercel AI SDK (provider abstraction), SolidJS (reactive terminal rendering), and Hono (HTTP server). It also enabled web and desktop frontends to share code with the core.

Key Numbers

120K+
GitHub Stars
800+
Contributors
5M+
Monthly Developers
75+
Providers Supported

What Makes It Different

  • Provider-agnostic. Works with 75+ AI providers: Anthropic, OpenAI, Google, AWS Bedrock, Azure, Ollama, local models, and more. You can switch models mid-conversation.
  • Open source. MIT license.
  • Multiple interfaces. Terminal TUI, desktop app (Tauri), Electron app, IDE extensions (VS Code, Zed, JetBrains, Neovim, Emacs), web interface, Slack bot, and a console mode.
  • Extensible. MCP servers, LSP integration, plugins with hooks, custom tools, custom agents, custom commands, custom skills, ACP support.
  • Event-sourced. Every message, tool call, and response is stored as an event. This enables undo/redo, session replay, and debugging.
Chapter 2

Architecture Overview

📖 10 min read

OpenCode follows a client-server architecture. When you run opencode, a Hono HTTP server starts and a client connects to it. The terminal TUI, web interface, and desktop app are all clients. They communicate with the server over HTTP and Server-Sent Events (SSE).

CLI Entry Hono HTTP Server Clients TUI / Web / Desktop Agent Loop SessionPrompt.loop() LLM Providers Vercel AI SDK Tool Registry bash, edit, grep... Permission System allow / deny / ask Event Bus pub/sub SQLite + Drizzle event sourcing MCP Servers stdio / HTTP / SSE LSP Integration diagnostics, hover Plugins hook system Snapshots shadow git repo Context Management auto-compact, pruning SDK @opencode-ai/sdk OpenCode Architecture - Client/Server with Event Sourcing
High-level architecture. The server holds all state and logic. Clients are views that connect over HTTP and SSE.

Monorepo Structure

OpenCode is a monorepo managed with Bun (package manager and runtime) and Turborepo (build orchestration).

Package Path Purpose
opencode packages/opencode/ Core package: agent loop, tools, providers, persistence, config, server, TUI. This is the main package.
@opencode-ai/app packages/app/ Web interface (SolidJS + Tailwind)
@opencode-ai/desktop packages/desktop/ Tauri desktop application
@opencode-ai/sdk packages/sdk/ TypeScript SDK for building custom clients

The core opencode package at packages/opencode/ contains everything: the agent loop, tool registry, provider integrations, persistence layer, HTTP server, and TUI. It is not split into separate core/server/tui packages.

The Effect Pattern

The codebase uses the Effect library heavily. Effect provides dependency injection and structured error handling for TypeScript.

Every major subsystem is an Effect Service: the database, the file system, the permission system, the LLM providers. Services declare their dependencies explicitly. The type system enforces that all dependencies are provided at runtime.

TypeScript
// Effect service pattern used throughout OpenCode
export class Database extends Effect.Service<Database>() {
  db: DrizzleDB
  query: <T>(sql: SQL) => Effect<T>
}

// At boot, all services compose into a single layer
const appLayer = Layer.mergeAll(
  DatabaseLive,
  PermissionLive,
  ProviderLive,
  ToolRegistryLive,
)

Key Design Decisions

  • Event sourcing. OpenCode stores every event (message sent, tool called, response received). Current state is derived by replaying events through projectors. This enables undo/redo and session replay.
  • Pub/sub event bus. Components communicate through an event bus. The TUI subscribes to events from the server. The persistence layer subscribes to agent events. No direct coupling.
  • Vercel AI SDK. A unified streaming interface across all LLM providers instead of provider-specific code.
  • Client-server split. The server holds all state and logic. Clients are views. Multiple clients can connect to the same server. You can build new frontends without touching the core.
Chapter 3

The Agent Loop

📖 15 min read

The agent loop is SessionPrompt.loop(). When you send a message, this function runs in a while loop: it builds the system prompt, streams the LLM response, executes any tool calls, and loops again. It exits when the LLM produces a response with no tool calls.

The Loop Visualized

User Message Build System Prompt Stream LLM Response Process Response Parts Tool calls? Yes / No No Return Response Yes Execute Tools loop back Doom loop detection 3 identical calls = abort The agent loop: while(true) that streams, processes, and loops on tool calls Auto-compact if needed (see Chapter 9)
The agent loop in SessionPrompt.loop(). Keeps running until the LLM produces no tool calls.

Building the System Prompt

Every iteration assembles a system prompt from multiple sources (see Chapter 12 for full details):

  1. Provider-specific base prompt. Selected based on the model ID. Claude models get anthropic.txt, GPT models get beast.txt or gpt.txt, etc.
  2. Environment block. Working directory, platform, date, git status, model name.
  3. Skills. Available skills listed in XML format.
  4. User instructions. From AGENTS.md, CLAUDE.md files (project-level and global).
  5. Structured output instruction. If applicable.
  6. User-provided system prompt. If set.

This assembly runs every iteration because context changes after tool execution.

Streaming the Response

LLM.stream() sends the full conversation to the provider and returns a stream of typed events. The stream produces these event types:

Event Type Description
reasoning-startBegins a reasoning/thinking block
reasoning-deltaIncremental reasoning content
reasoning-endEnds a reasoning block
text-startBegins a text response
text-deltaIncremental text content
text-endEnds a text response
tool-input-startBegins streaming tool call arguments
tool-input-deltaIncremental tool argument JSON
tool-input-endEnds tool call argument streaming
tool-callComplete tool call with name and parsed arguments
tool-resultResult returned from tool execution
tool-errorError from tool execution
start-stepBegins a new agent loop step
finish-stepEnds a step, includes token usage
TypeScript - Simplified stream processing
for await (const part of stream.fullStream) {
  switch (part.type) {
    case "text-delta":
      currentText += part.textDelta
      break

    case "reasoning":
      currentReasoning += part.textDelta
      break

    case "tool-call":
      toolCalls.push({
        name: part.toolName,
        args: part.args,
      })
      break
  }
}

Tool Execution

After the stream completes, the loop checks for tool calls. If present, each tool is executed. Results are appended to the conversation as tool result messages, and the loop iterates again.

If no tool calls were made, the loop exits and returns the response.

Subtask Handling

When the task tool is called, a child session is spawned with its own agent loop. The parent agent can fire off multiple tasks in parallel and continue working. Each subtask runs as a full agent with its own tool access (determined by the sub-agent definition; see Appendix A). Task results are returned to the parent as tool results.

Doom Loop Detection

If the LLM makes the same tool call three times in a row (same tool, same arguments), the loop aborts. This prevents burning tokens when the LLM gets stuck retrying a failing operation.

Step Tracking and Usage

Each loop iteration is a "step." OpenCode tracks step count and token usage (input, output, cache hits) across all steps. This data is exposed in the UI.

Auto-Title Generation

After the first response in a new session, a background LLM call generates a short title (50 characters or fewer) using the title agent. This title appears in the session list.

Chapter 4

Tool System

📖 12 min read

Tools are how the LLM interacts with the filesystem, shell, and external services. Without tools it is a chatbot. With tools it can read files, write code, run tests, search the web, and spawn sub-agents.

How Tools Are Defined

Every tool is created with Tool.define(). Each definition specifies a name, description, Zod input schema, execute function, and permission requirements.

Tool descriptions are loaded from .txt files alongside the tool source code. These text files support template variables that get interpolated at runtime (e.g., the current working directory, OS platform). This keeps long descriptions out of the TypeScript code and makes them easy to edit.

TypeScript - Tool definition pattern
export const ReadTool = Tool.define({
  name: "read",
  description: "Read the contents of a file",
  parameters: z.object({
    filePath: z.string().describe("Absolute path to the file"),
    offset: z.number().optional(),
    limit: z.number().optional(),
  }),
  async execute({ filePath, offset, limit }) {
    const content = await fs.readFile(filePath, "utf-8")
    // apply offset/limit, return content
    return { content }
  },
})

Built-in Tools

Tool Description Permission
bash Execute shell commands in the project directory Ask
read Read file contents with optional line range Allow
write Create or overwrite files Ask
edit Targeted find-and-replace edits in files Ask
apply_patch Apply a unified diff patch to a file. Used instead of edit/write for GPT models. Ask
glob Find files matching a glob pattern Allow
grep Search file contents with regex Allow
list List files in a directory Allow
task Spawn a sub-agent for parallel work Allow
webfetch Fetch and extract content from a URL Ask
websearch Search the web and return results Ask
codesearch Semantic code search using embeddings Allow
todoread Read the current task list Allow
todowrite Create or update tasks in the task list Allow
lsp Query language servers (diagnostics, definitions, hover) Allow
skill Load a skill file's contents into the conversation Allow
invalid Returned when the LLM calls a tool that does not exist. Provides an error message. Allow
batch Execute multiple tool calls in a single batch (experimental) Varies
plan_exit Exit plan mode and return to build mode (experimental) Allow

Tool Filtering per Model

Not all models get the same tool set. GPT models receive apply_patch instead of edit and write, because GPT models perform better with unified diffs than with the find-and-replace style of edit. The tool set is filtered based on the model ID before being sent to the LLM.

The Permission Model

Permissions have three levels:

  • Allow - Runs immediately. Used for read-only operations.
  • Ask - Requires user approval. Used for write operations and external interactions.
  • Deny - Blocked entirely.

Permissions are stored as a Ruleset, an array of rules. Each rule has a tool name and a glob pattern to match against the tool's arguments. Rules are evaluated in order; the first match wins.

JSON - Permission configuration
{
  "permissions": {
    "bash": {
      "allow": ["npm test*", "npm run lint"],
      "deny": ["rm -rf *"]
    }
  }
}

When a tool call comes in, the permission system serializes the tool arguments into a string and matches it against the Ruleset patterns. If no rule matches, the tool's default permission level applies.

Custom Tools

Custom tools are loaded from .opencode/tool/*.{js,ts} or .opencode/tools/*.{js,ts} at startup. They are registered alongside built-in tools and appear to the LLM the same way. This is useful for project-specific workflows like deploying, running custom test suites, or calling internal APIs.

The Task Tool (Sub-agents)

The task tool spawns a child session with its own agent loop. The parent can fire off multiple tasks in parallel. Each task is a full agent with tool access defined by its agent configuration (the general sub-agent by default, or explore for read-only searches). Sub-tasks can spawn their own sub-tasks.

Chapter 5

Provider System

📖 10 min read

OpenCode supports 75+ AI providers through an abstraction layer built on the Vercel AI SDK. Each provider has its own SDK package. The Vercel AI SDK wraps them in a unified streamText() interface. OpenCode adds a thin layer on top for token tracking, event emission, and error handling.

Provider SDK Packages

These are the SDK packages OpenCode uses:

Provider SDK Package
Anthropic@ai-sdk/anthropic
OpenAI@ai-sdk/openai
Google (Vertex / AI Studio)@ai-sdk/google
AWS Bedrock@ai-sdk/amazon-bedrock
Azure OpenAI@ai-sdk/azure
xAI (Grok)@ai-sdk/xai
Mistral@ai-sdk/mistral
Groq@ai-sdk/groq
OpenRouter@openrouter/ai-sdk-provider
DeepInfra@ai-sdk/deepinfra
Cerebras@ai-sdk/cerebras
Cohere@ai-sdk/cohere
Together AI@ai-sdk/togetherai
Perplexity@ai-sdk/perplexity
Vercel@ai-sdk/vercel
GitLabgitlab-ai-provider
GitHub CopilotCustom Copilot SDK
Ollama / OpenAI-compatible@ai-sdk/openai (with custom baseURL)

Model Catalog

OpenCode maintains a model catalog sourced from models.dev. It stores: context window size, tool use support, extended thinking support, pricing per token, and max output tokens. This metadata drives context management, auto-compact thresholds, and cost display.

LLM.stream() Internals

When the agent loop calls LLM.stream():

  1. The provider and model are resolved from config.
  2. The model is wrapped with wrapLanguageModel middleware. This middleware layer handles token tracking, event emission, and provider-specific adjustments.
  3. For LiteLLM proxy compatibility, a dummy _noop tool is injected. Some proxies fail when no tools are provided, so this ensures the tools array is never empty.
  4. streamText() from the Vercel AI SDK is called with the wrapped model, system prompt, messages, and tool schemas.
  5. The resulting stream and usage metadata are returned.
TypeScript - Simplified LLM.stream()
async function stream(options) {
  const provider = resolveProvider(options.model)
  const wrapped = wrapLanguageModel(provider, middleware)

  const result = await streamText({
    model: wrapped,
    system: options.systemPrompt,
    messages: options.messages,
    tools: options.toolSchemas,
    maxTokens: options.maxTokens,
  })

  return {
    fullStream: result.fullStream,
    usage: result.usage,
  }
}

Provider-Specific Handling

  • Anthropic gets beta headers for prompt caching and extended thinking. The system prompt uses anthropic.txt which is optimized for Claude.
  • OpenAI supports both Chat Completions API and Responses API.
  • Google Gemini can return multiple tool calls in a single message chunk.
  • Local models via Ollama use the OpenAI-compatible SDK with a custom baseURL.

You can switch models mid-conversation. The conversation history is serialized and continues with the new model.

Chapter 6

The Server & API

📖 10 min read

OpenCode runs a Hono HTTP server on a local port. All clients -- including the built-in TUI -- communicate with this server. This is what enables multiple frontends to connect to the same running instance.

Server Configuration

  • CORS: Allows localhost, tauri://, and *.opencode.ai origins.
  • Auth: Optional basic auth via the OPENCODE_SERVER_PASSWORD environment variable.
  • Compression: Gzip compression enabled.
  • Instance bootstrapping: A middleware initializes the project instance (database, config, tools) before handling requests.

API Routes

The full route table is in Appendix C. Here is the high-level grouping:

Hono Server Sessions Messages Events (SSE) GET /session POST /session GET /session/:id DEL /session/:id GET /:id/message POST /:id/message POST /:id/abort POST /:id/revert GET /event (SSE stream) GET /global/event (global SSE) Config & Auth Permissions & MCP Files, LSP & more GET /config PATCH /config PUT /auth/:providerID GET /permission POST /permission GET /mcp GET /file GET /lsp WS /pty All communication goes through HTTP endpoints, including the TUI
Server API surface (subset). Full route table in Appendix C.

SSE Event Streaming

Clients connect to GET /event for a Server-Sent Events stream scoped to the current session. GET /global/event provides a global stream across all sessions. Events pushed down this stream include:

  • message.updated - A message part was updated (text streaming in, tool result arrived)
  • part.updated, part.delta - Part-level updates and incremental content
  • session.created, session.updated - Session lifecycle
  • permission.asked - A tool needs user approval

The TUI, web interface, and desktop app all subscribe to the same event stream. All frontends stay in sync.

Multi-Instance Support

Each project directory gets its own server on a different port, with its own database and session list. The port is determined by hashing the project directory path, so reopening the same project always gets the same port.

The SDK Client

The @opencode-ai/sdk package provides a TypeScript client for the server API. It supports two transport modes:

  • HTTP transport. For external clients connecting over the network.
  • In-process transport. For the TUI, which runs in the same process as the server. Skips the network layer.
TypeScript - Using the SDK
import { OpenCode } from "@opencode-ai/sdk"

const client = new OpenCode({ url: "http://localhost:3000" })

const session = await client.session.create()
await client.message.create(session.id, {
  content: "Refactor the auth module",
})

client.on("message.updated", (msg) => {
  console.log(msg.content)
})
Chapter 7

The Terminal UI

📖 10 min read

The terminal UI is the primary interface for OpenCode. It is built on OpenTUI, a custom terminal rendering framework that uses SolidJS for reactive updates.

OpenTUI

OpenTUI uses SolidJS instead of a virtual DOM. When state changes, SolidJS updates only the terminal cells that depend on that state. This avoids full re-renders.

OpenTUI provides:

  • A component model (JSX components that render to terminal cells)
  • Flexbox-like layout for positioning
  • Reactive state via SolidJS signals
  • Keyboard event handling
  • Text wrapping, scrolling, and ANSI color support

Component List

The TUI is composed of these dialog and UI components:

Component Purpose
dialog-agentAgent/mode selection dialog
dialog-commandCommand palette (fuzzy-searchable)
dialog-mcpMCP server management
dialog-modelModel picker
dialog-providerProvider configuration
dialog-session-listSession browser
dialog-skillSkill selection and preview
dialog-statusStatus information display
dialog-theme-listTheme picker
dialog-workspace-listWorkspace browser
promptInput prompt area with autocomplete

Provider Architecture

The TUI uses a provider pattern (like React context) to inject services into the component tree:

Provider What It Does
ThemeColors, styling, visual configuration
SDKConnection to the OpenCode server
SyncReal-time state synchronization via SSE events
DialogModal dialog management
CommandCommand palette and slash command registry
KeybindKeyboard shortcut management

How the TUI Connects to the Server

The TUI connects via the in-process SDK transport. It subscribes to the SSE event stream and maintains a local reactive store. When you type a message, it sends POST /session/:id/prompt. SSE events stream in and update the UI in real-time: text appears incrementally, tool calls show progress, permission requests appear as dialogs.

Build and Plan Modes

Switch between modes with Tab:

  • Build mode. Full tool access. Can read, write, edit files, run commands, spawn tasks.
  • Plan mode. Read-only. Can read files and search, but cannot modify anything. Can only edit plan files.

The Command Palette

Press Ctrl+K / Cmd+K to open the command palette. It provides fuzzy-searchable access to: model switching, agent selection, session management, configuration, and all registered commands.

Chapter 8

Data & Persistence

📖 15 min read

OpenCode stores all state in SQLite via Drizzle ORM. The database lives at ~/.local/share/opencode/opencode.db (or opencode-{channel}.db for non-default channels). SQLite is configured with WAL mode, NORMAL sync, 5-second busy timeout, 64MB cache, and foreign keys enabled.

The persistence layer uses event sourcing: instead of storing current state directly, it stores every event that occurred. Current state is computed by replaying events through projector functions that materialize the data into relational tables.

Database Schema

There are 8 tables. The three core tables (session, message, part) store conversation data. The message and part tables use a data JSON column for type-specific fields, with only the IDs and foreign keys as dedicated columns.

Table Columns Purpose
session id, project_id (FK), workspace_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs (JSON), revert (JSON), permission (JSON Ruleset), time_created, time_updated, time_compacting, time_archived Each conversation. parent_id links subtask sessions to their parent. revert stores undo state.
message id, session_id (FK, cascade), time_created, time_updated, data (JSON) Messages in a session. The data JSON contains: role (user/assistant), tokens, cost, error, parentID (links assistant replies to user messages), modelID, finish reason, summary flag.
part id, message_id (FK, cascade), session_id, time_created, time_updated, data (JSON) Typed parts within a message. The data JSON contains the part type and all type-specific fields.
todo session_id, content, status, priority, position Task list items. Composite PK: (session_id, position).
permission project_id (PK), data (JSON Ruleset), time_created, time_updated Per-project permission rules.
project id, name, worktree Registered projects.
event_sequence aggregate_id (PK), seq Tracks the latest sequence number per aggregate (session).
event id, aggregate_id (FK, cascade), seq, type, data (JSON) The event log. Every state change recorded as a typed event.

Messages and Parts

The core data model is: a session contains messages, and each message contains parts. A single assistant message typically contains multiple parts — some text, possibly reasoning, and one or more tool calls.

Messages carry metadata in their data JSON: the role, token counts, cost, error state, the model ID used, finish reason, and a parentID field that links assistant messages back to the user message that triggered them. Assistant messages also have a summary boolean flag (true for compaction summary messages) and a finish flag (true when the response is complete).

The Part Type System

Every part has a base of { id, sessionID, messageID } plus type-specific fields:

Part Type Fields Description
TextPart text, synthetic?, ignored?, time, metadata? Plain text from the LLM. synthetic marks injected text (e.g. mode-switch reminders). ignored excludes it from LLM conversion.
ReasoningPart text, time, metadata? Extended thinking/reasoning content (e.g. Claude's thinking blocks).
ToolPart callID, tool, state, metadata? A tool invocation. The state field is a discriminated union that transitions through a state machine (see below).
FilePart mime, filename?, url, source? File attachment. Source can be a file path, an LSP symbol reference, or an MCP resource.
CompactionPart auto, overflow? Marks a compaction boundary on a user message. Messages before this are hidden when loading conversation for the LLM.
SubtaskPart prompt, description, agent, model?, command? A pending subtask to be executed by the task tool.
SnapshotPart snapshot Git tree hash representing filesystem state at a point in time.
PatchPart hash, files[] Records which files changed between two snapshots. hash is the "before" tree hash. Used for revert.
AgentPart name, source? An agent mention/reference.
RetryPart attempt, error Records an API retry. Error includes statusCode, isRetryable, responseHeaders.
StepStartPart snapshot? Marks the beginning of an agent loop step. Snapshot is the git tree hash at step start.
StepFinishPart reason, snapshot?, cost, tokens Marks end of a step. Tokens includes: input, output, reasoning, cache.read, cache.write.

ToolPart State Machine

The state field on a ToolPart is a discriminated union on status that transitions through four states:

pending input: {}, raw: "" tool-call running input, time.start tool-result completed output, title, time.end tool-error error error string, time.end Aborted tools also go to error
ToolPart state transitions during stream processing

State fields by status:

  • pending: input: {}, raw: "" — created at tool-input-start stream event. Raw input string accumulates via tool-input-delta.
  • running: input (parsed args), title?, metadata?, time.start — transitions at tool-call event when input is fully parsed.
  • completed: input, output, title, metadata, time.start, time.end, time.compacted?, attachments? — transitions at tool-result. The time.compacted field is set later during pruning.
  • error: input, error (string), metadata?, time.start, time.end — transitions at tool-error, or when the stream ends with tools still in pending/running state (set to "Tool execution aborted").

How Parts Are Created During Streaming

SessionProcessor.create() iterates over the LLM stream and creates/updates parts in real time:

  1. reasoning-start → creates a ReasoningPart with empty text and time.start = Date.now()
  2. reasoning-delta → appends text, publishes delta event to the bus for UI updates
  3. reasoning-end → trims text, sets time.end
  4. text-start → creates a TextPart with empty text
  5. text-delta → appends text, publishes delta
  6. text-end → trims text, runs plugin hook experimental.text.complete, sets time.end
  7. tool-input-start → creates ToolPart in pending state
  8. tool-call → transitions to running, checks for doom loop (3 identical calls triggers permission ask)
  9. tool-result → transitions to completed with output, title, metadata, attachments
  10. tool-error → transitions to error
  11. start-step → calls Snapshot.track() to capture filesystem state, creates StepStartPart
  12. finish-step → computes token usage and cost, creates StepFinishPart, creates PatchPart if files changed since the step's snapshot

All part creation and updates go through Session.updatePart(), which calls SyncEvent.run(MessageV2.Event.PartUpdated, ...). This persists the event and runs the projector to upsert the part row.

Converting Messages to LLM Format

MessageV2.toModelMessages() converts stored messages and parts into the format expected by the Vercel AI SDK:

  • User messages: TextParts become { type: "text", text }. FileParts become { type: "file", url, mediaType }. CompactionParts become { type: "text", text: "What did we do so far?" }.
  • Assistant messages: TextParts and ReasoningParts map directly. Completed ToolParts include the output (or "[Old tool result content cleared]" if time.compacted is set). Error ToolParts include the error text. Pending/running ToolParts become "[Tool execution was interrupted]".
  • Skipped messages: Assistant messages with errors are skipped entirely, unless the error is an AbortedError and the message has substantive parts.

For providers that don't support media in tool results (anything other than Anthropic, OpenAI, Bedrock, Vertex, or Gemini 3+), media attachments are extracted from tool results and injected as a separate user message after the assistant message.

Loading Messages: filterCompacted

MessageV2.filterCompacted() loads messages for the LLM, respecting compaction boundaries. It iterates messages in reverse chronological order (newest first). When it finds an assistant message that is a completed summary (summary: true, has finish, no error), it records that message's parentID. When it then reaches the corresponding user message and that message has a CompactionPart, it stops. Everything older is excluded.

The result is: the compaction user message + its summary response + all subsequent messages. The summary replaces all prior conversation history.

Event Sourcing

Events are defined with SyncEvent.define({ type, version, aggregate, schema }). The aggregate field names the key used as the aggregate ID (always "sessionID" for session events).

Seven event types exist:

Event Type Data Projector Action
session.created sessionID, info (full Session.Info) INSERT into session table
session.updated sessionID, info (partial Session.Info) UPDATE session row (only changed fields)
session.deleted sessionID, info DELETE session row (cascades to messages, parts)
message.updated sessionID, info (MessageV2.Info) UPSERT into message table (insert or update on conflict)
message.removed sessionID, messageID DELETE message row
message.part.updated sessionID, part (full Part), time UPSERT into part table
message.part.removed sessionID, messageID, partID DELETE part row

SyncEvent.run() executes within an immediate SQLite transaction: reads the current sequence number for the aggregate, increments it, runs the projector (which does the actual INSERT/UPDATE/DELETE), optionally persists the event to the event table (when workspace sync is enabled), then emits the event on the bus after commit.

Events can be replayed via SyncEvent.replay() for workspace sync. Replay checks sequence ordering and silently skips already-applied events (idempotent).

Fork, Revert, and Undo

Fork (Session.fork()) creates a new session and copies messages up to a specified point. Each message and part gets a new ID. The parentID links on assistant messages are remapped to the new message IDs.

Revert (SessionRevert.revert()) restores files to a previous state without deleting messages:

  1. Walks all messages to find the revert target (by messageID and optional partID)
  2. Collects all PatchParts that occur after the target
  3. Takes a snapshot of current state
  4. For each file in each patch: checks out the file from the patch's git tree hash, or deletes it if it didn't exist at that point
  5. Stores revert metadata on the session: { messageID, partID?, snapshot?, diff? }

Cleanup happens when the user sends the next message after a revert. Messages after the revert point are deleted via SyncEvent.run(MessageV2.Event.Removed). If the revert was mid-message, parts from the revert point onward are also deleted.

Unrevert (SessionRevert.unrevert()) restores the full git tree to what it was before the revert, then clears the revert field.

Snapshots: The Shadow Git Repo

OpenCode maintains a shadow Git repository separate from your working repo. It lives at <data-dir>/snapshot/<projectID>/<hash(worktree)>/. All operations are serialized per directory using a semaphore.

Operations:

  • init() — creates the git directory with autocrlf=false, longpaths=true, symlinks=true, fsmonitor=false.
  • track() — finds changed/new files via git diff-files and git ls-files --others, filters out files over 2MB, runs git add and git write-tree. Returns the tree hash.
  • patch(hash) — stages current state, diffs against the given tree hash, returns the list of changed files.
  • restore(snapshot) — runs git read-tree + git checkout-index -a -f to restore the full working tree.
  • revert(patches) — selective per-file revert: git checkout <hash> -- <file> for each file, or deletes if the file didn't exist.
  • diff(hash) / diffFull(from, to) — computes diffs between tree hashes, returning structured FileDiff[] with before/after content.
  • cleanup() — runs git gc --prune=7.days, scheduled hourly.
Chapter 9

Context Window Management

📖 12 min read

Every LLM has a context window limit. In a long coding session with many tool calls, you can hit it fast. OpenCode manages this automatically with a two-phase system.

Overflow Detection

Before each loop iteration, OpenCode checks whether the context is approaching the limit. The formula:

Overflow check
overflow = totalTokens >= inputLimit - reserved

// reserved = min(20_000, maxOutputTokens)

inputLimit is the model's context window size minus max output tokens. reserved defaults to the smaller of 20,000 tokens or maxOutputTokens. If totalTokens exceeds this threshold, the two-phase cleanup runs.

Token Thresholds

Two constants control pruning behavior:

  • PRUNE_PROTECT = 40,000 tokens. The most recent 40K tokens of conversation are never pruned.
  • PRUNE_MINIMUM = 20,000 tokens. A tool output must be at least 20K tokens old (measured from the end of conversation) before it is eligible for pruning.

The Two-Phase Approach

Messages accumulate over time Over limit? No Continue Yes Phase 1 Prune old tool outputs Still over? No Done Yes Phase 2 Compact with LLM Summary replaces old messages Two-phase context management: prune first, then compact if needed
Phase 1 prunes tool outputs without an LLM call. Phase 2 uses the compaction agent to summarize older messages.

Phase 1: Pruning

Pruning does not call the LLM. It finds old tool call results (older than PRUNE_MINIMUM tokens from the end of conversation) and replaces their output with a placeholder like [output pruned]. The tool call record stays in the history so the LLM knows a tool was used, but the large result body is removed.

The most recent PRUNE_PROTECT tokens (40K) are never pruned.

Phase 2: Compaction

If pruning is not enough, the older portion of the conversation is sent to the compaction agent. This agent produces a summary that captures the goal, key decisions, discoveries, and current state. The summary is inserted as a CompactionPart at the beginning, and the old messages it summarizes are removed.

The compaction summary follows a structured template (see Appendix B).

Plugin Hook

The experimental.session.compacting plugin hook fires before compaction. Plugins can use this to customize compaction behavior or inject additional context into the summary.

Chapter 10

Extensibility

📖 12 min read

OpenCode is designed to be extended at every level: custom tools, custom agents, custom commands, plugins with hooks, MCP servers, LSP integration, and ACP support.

MCP (Model Context Protocol)

OpenCode has first-class MCP support. Three transport types are supported:

  • StdioClientTransport. Runs a local process. Communicates over stdin/stdout.
  • StreamableHTTPClientTransport. Connects to a remote HTTP server with streaming.
  • SSEClientTransport. Connects to a remote server using Server-Sent Events.

MCP servers that require authorization are handled via OAuth (RFC 7591 dynamic client registration).

JSON - .opencode/config.json with MCP servers
{
  "mcp": {
    "servers": {
      "database": {
        "type": "stdio",
        "command": "npx",
        "args": ["@modelcontextprotocol/server-postgres"]
      },
      "github": {
        "type": "sse",
        "url": "https://mcp.github.com/sse"
      }
    }
  }
}

Once connected, MCP tools appear alongside built-in tools. The LLM uses them the same way.

LSP Integration

OpenCode auto-detects language servers (tsserver, pyright, gopls, etc.) and connects to them. The agent gets access to diagnostics, hover information, go-to-definition, and symbol search. LSP diagnostics are injected into the system prompt so the agent knows about current errors.

Plugins

Plugins hook into lifecycle events. The full list of hooks:

Hook When It Fires
chat.paramsBefore an LLM call. Can modify system prompt, parameters.
chat.headersBefore an LLM HTTP request. Can add/modify headers.
tool.definitionWhen tool definitions are assembled. Can modify tool schemas.
tool.execute.beforeBefore a tool runs. Can validate or modify arguments.
tool.execute.afterAfter a tool runs. Can post-process results.
experimental.chat.system.transformTransform the system prompt before sending.
experimental.chat.messages.transformTransform the message array before sending.
experimental.text.completeText completion hook for autocomplete.
experimental.session.compactingBefore compaction runs.

Built-in Plugins

OpenCode ships with authentication plugins for specific providers:

  • CodexAuthPlugin - Handles OpenAI Codex authentication
  • CopilotAuthPlugin - Handles GitHub Copilot OAuth flow
  • GitlabAuthPlugin - Handles GitLab authentication
  • PoeAuthPlugin - Handles Poe authentication

Custom Commands

Markdown files in .opencode/command/**/*.md or .opencode/commands/**/*.md register as slash commands. The file's content becomes the prompt sent to the agent when the command is invoked. Commands appear in the command palette.

Custom Agents

Markdown files in .opencode/agent/**/*.md or .opencode/agents/**/*.md define custom agents. Each file specifies the agent's system prompt, tool access, and behavior. Custom agents appear alongside the built-in build and plan agents.

Custom Tools

JavaScript or TypeScript files in .opencode/tool/*.{js,ts} or .opencode/tools/*.{js,ts} are loaded at startup and registered as tools.

IDE Extensions

OpenCode has extensions for VS Code, Zed, JetBrains, Neovim, and Emacs. These connect to the same OpenCode server and share sessions, tools, and config.

The VS Code extension is installed via code --install-extension sst-dev.opencode. OpenCode detects IDE environments through the TERM_PROGRAM and GIT_ASKPASS environment variables.

ACP (Agent Client Protocol)

ACP allows agents to communicate with each other. OpenCode can be orchestrated by a larger system or orchestrate other ACP-compatible agents.

Chapter 11

Desktop & Beyond

📖 8 min read

The client-server architecture means OpenCode can run anywhere a browser or native app can.

Tauri Desktop App

The primary desktop app uses Tauri (Rust-based). It wraps the web interface in a native window.

  1. Starts the OpenCode server as a child process
  2. Opens a native window with the web UI
  3. The web UI connects to the local server over HTTP + SSE
  4. On close, it shuts down the server

Provides native OS integration: system notifications, file dialogs, menu bar, keyboard shortcuts.

Electron Desktop App

An Electron-based desktop app is also available as an alternative to Tauri. It provides the same web UI in an Electron shell.

Web Interface

The web interface is a SolidJS + Tailwind application in the @opencode-ai/app package. It provides markdown rendering, syntax-highlighted code diffs, file tree views, and works with any OpenCode server.

Slack Integration

A Slack bot connects to an OpenCode server. You message the bot in a channel or DM, and it responds with full agent capabilities including tool use and streaming. Useful for team collaboration -- everyone in the channel sees the agent work in real-time.

Console App

A stripped-down console mode for environments where the full TUI is not available (CI/CD pipelines, basic SSH sessions). Works with plain text input/output.

IDE Extensions

Extensions for VS Code, Zed, JetBrains, Neovim, and Emacs. They are alternative frontends that connect to the same server.

VS Code: code --install-extension sst-dev.opencode. OpenCode detects IDE environments via TERM_PROGRAM and GIT_ASKPASS environment variables to adjust behavior (e.g., opening files in the editor instead of printing contents).

The SDK

Everything above is built on the @opencode-ai/sdk package. It provides full TypeScript types, session management, message streaming, SSE event subscription, permission handling, and configuration management. Use it to build custom frontends.

Chapter 12

System Prompt Construction

📖 15 min read

The system prompt is assembled dynamically every iteration of the agent loop. It determines how the LLM behaves, what it knows about, and what instructions it follows. This chapter covers the exact assembly process.

Provider-Specific Base Prompts

OpenCode selects a base prompt based on the model ID. Prompt files live in src/session/prompt/:

File Matched When Key Characteristics
anthropic.txt Model ID contains "claude" Opens with "You are OpenCode, the best coding agent on the planet." Covers TodoWrite task management, Task tool usage, code reference format.
beast.txt Model ID contains "gpt-4", "o1", or "o3" Aggressive autonomous behavior: "MUST iterate and keep going until the problem is solved." Requires internet research.
codex.txt Model ID contains "gpt" AND "codex" Stripped-down. Prefers apply_patch. ASCII-only output. Git hygiene rules.
gpt.txt Model ID contains "gpt" (after codex check) Commentary/final channel system. Varied personality.
gemini.txt Model ID contains "gemini-" Long prompt with core mandates, editing constraints, new application workflow.
trinity.txt Model ID contains "trinity" Copilot-style formatting. File reference rules.
default.txt Fallback (no other match) "You are opencode, an interactive CLI tool..." Emphasizes conciseness ("fewer than 4 lines"), proactiveness rules, "DO NOT ADD ANY COMMENTS".

Selection logic checks the model ID in this order: "claude" -> "gpt-4"/"o1"/"o3" (beast) -> "gpt"+"codex" -> "gpt" -> "gemini-" -> "trinity" -> default.

Assembly Order

The final system prompt is composed in this order:

  1. Agent prompt (if custom) OR provider-specific base prompt. If the current agent has a custom prompt defined, that is used. Otherwise the provider-specific prompt from the table above.
  2. Environment block. Model name, working directory, worktree root, platform, date, git status.
  3. Skills listing. Available skills in XML format.
  4. User instructions. AGENTS.md, CLAUDE.md files from project and global paths.
  5. Structured output instruction. If the agent requires structured output.
  6. User-provided system prompt. If set in config.

Environment Block Format

Environment block template
You are powered by the model named {model.api.id}. The exact model ID is {providerID}/{model.api.id}
Here is some useful information about the environment you are running in:
<env>
  Working directory: {Instance.directory}
  Workspace root folder: {Instance.worktree}
  Is directory a git repo: {yes|no}
  Platform: {process.platform}
  Today's date: {new Date().toDateString()}
</env>

Instruction Loading

OpenCode searches for instruction files named AGENTS.md, CLAUDE.md, and CONTEXT.md (deprecated). These are loaded from two locations:

  • Project. Walks up from the current working directory to the worktree root, collecting every matching file found along the way.
  • Global. Checks ~/.config/opencode/ and ~/.claude/.

Additionally, contextual instructions are loaded when files are read during the session. When the agent reads a file, OpenCode walks up from that file's directory looking for AGENTS.md/CLAUDE.md and injects any found instructions into the prompt.

Skills Format in Prompt

Skills are listed in the prompt using XML:

XML - Skills in system prompt
<available_skills>
  <skill>
    <name>{name}</name>
    <description>{description}</description>
    <location>{file URL}</location>
  </skill>
</available_skills>

Skills are markdown files in .opencode/skills/. They are not loaded into the prompt by default -- the agent sees the listing and uses the skill tool to load one when needed.

Appendix A

Agent Definitions

OpenCode has seven built-in agents. Each has a mode, tool access rules, and sometimes a custom prompt.

Agent Mode Description Tool Access
build primary Default agent. Full tool access. This is what runs when you type a message normally. All tools. question and plan_enter permissions allowed.
plan primary Read-only. Can only edit plan files. Used for analysis and planning without modifying the codebase. Read-only tools. plan_exit allowed. Write tools denied.
general subagent For multi-step tasks spawned via the task tool. Full tool access minus todowrite. All tools except todowrite (denied).
explore subagent Read-only file search specialist. Has a custom prompt optimized for finding information. Only: grep, glob, list, bash, webfetch, websearch, codesearch, read.
compaction primary (hidden) Generates conversation summaries during context compaction. No tools. All tools denied.
title primary (hidden) Generates conversation titles (50 chars or fewer). Temperature 0.5. No tools. All tools denied.
summary primary (hidden) Generates PR-style summaries of conversations. No tools. All tools denied.

Hidden agents are not shown in the agent picker. They are used internally by the system.

Appendix B

Prompt Templates

Key prompt templates used by the internal agents and system features.

Compaction Prompt

The compaction agent receives a user message with this structure:

Compaction summary template
Summarize this conversation so far. Include:

**Goal**: What was the user trying to accomplish?
**Instructions**: What specific instructions or constraints did the user give?
**Discoveries**: What was learned along the way? (errors found, patterns identified, etc.)
**Accomplished**: What has been done so far?
**Relevant files**: List any files that were read, modified, or are relevant to the task.

The compaction agent's system prompt instructs it to produce a concise summary following this template. The result replaces the older messages it summarized.

Title Prompt

The title agent's system prompt:

Title agent behavior
You are a title generator. You output ONLY a thread title.
The title must be 50 characters or fewer.
No quotes. No punctuation at the end. No prefixes.

Temperature is set to 0.5 for slightly varied but focused output.

Mode-Switching Reminders

When switching between modes, reminder messages are injected:

  • Plan mode active: A message reminds the agent it is in plan mode, can only read files and think, and should use plan_exit to return to build mode.
  • Build switch: A message reminds the agent it has returned to build mode with full tool access.
  • Max steps reached: When the step count hits the configured maximum, a message tells the agent to wrap up and provide a summary.

Mid-Loop User Messages

If the user sends a message while the agent is still processing (mid-loop), the message is wrapped in <system-reminder> tags and injected into the conversation. This allows the user to provide corrections or additional context without waiting for the agent to finish.

Mid-loop message wrapping
<system-reminder>
{user's message text}
</system-reminder>
Appendix C

Complete API Reference

All HTTP routes exposed by the OpenCode server.

Method Route Description
GET/sessionList all sessions
POST/sessionCreate a new session
GET/session/:idGet session by ID
DELETE/session/:idDelete a session
GET/session/:id/messageList messages in a session
POST/session/:id/messageAdd a message to a session
POST/session/:id/promptSubmit a prompt (starts the agent loop)
POST/session/:id/abortAbort the running agent loop
POST/session/:id/compactTrigger manual compaction
POST/session/:id/revertRevert to a previous state (undo)
POST/session/:id/forkFork a session at a given point
POST/session/:id/shareGenerate a shareable link for a session
GET/eventSSE stream for session events
GET/globalGlobal session data
GET/global/eventGlobal SSE stream (all sessions)
GET/projectGet project info
GET/providerList available providers
GET/configGet current configuration
PUT/configUpdate configuration
GET/mcpList MCP servers
POST/mcpAdd an MCP server
DELETE/mcpRemove an MCP server
GET/permissionList permission rules
POST/permissionAdd/update permission rules
GET/questionGet pending permission questions
POST/questionAnswer a permission question
WS/ptyWebSocket for pseudo-terminal access
GET/fileRead a file
GET/auth/:providerIDGet auth status for a provider
POST/auth/:providerIDInitiate auth for a provider
DELETE/auth/:providerIDRemove auth for a provider
GET/agentList available agents
GET/skillList available skills
GET/lspGet LSP status and diagnostics
GET/commandList available commands
GET/pathGet path information
GET/vcsGet version control status
Appendix D

Event Bus Events

Events published on the internal event bus. These are the events that flow through SSE to connected clients.

Event Description
session.createdA new session was created
session.updatedSession metadata changed (title, status, model)
session.deletedA session was deleted
session.compactedA session was compacted (older messages summarized)
message.updatedA message was updated (new parts, content changes)
part.updatedA message part was updated
part.deltaIncremental content added to a part (streaming text)
permission.askedA tool is requesting user permission
permission.repliedUser responded to a permission request
lsp.updatedLSP diagnostics or status changed
mcp.tools.changedMCP server tools were added, removed, or changed
server.connectedA client connected to the server
server.heartbeatPeriodic heartbeat for connection keepalive
server.instance.disposedThe server instance is shutting down