Effect Service Architecture

OpenCode v2 is built on Effect-TS, a TypeScript framework for composable, type-safe services. Understanding the Effect patterns used in OpenCode helps you navigate the codebase and write plugins.

Service Pattern

Core services follow a consistent pattern: a Context.Service definition, a Layer that provides the implementation, and an Interface that describes the contract.

// From packages/core/src/agent.ts (simplified)
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}

const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const bus = yield* Bus.Service
    const global = yield* Global.Service
    // ... implementation
    return { get, resolve, select, list, transform }
  }),
)

Key patterns:

  • Services are identified by a string tag (e.g. "@opencode/Agent")
  • Effect.gen generators compose service dependencies via yield*
  • Services bind to named variables before calling methods (not nested yields)
  • Layer.effect constructs the service from its dependencies

Core Services

ServiceTagRole
Agent.Service@opencode/AgentAgent registry, resolution, selection
Session@opencode/SessionSession lifecycle, persistence, runner
Tool@opencode/ToolTool registration, snapshots, execution
Permission@opencode/PermissionPermission rules, evaluation
Config@opencode/ConfigConfig loading, merging, normalization
Bus@opencode/BusInternal event bus
MCP@opencode/MCPMCP server management
PluginRuntime@opencode/PluginRuntimePlugin lifecycle, hooks

Session Runner

The SessionRunner coordinates the agentic loop. It lives in packages/core/src/session/runner/ and manages:

  • Message generation via the AI SDK
  • Tool dispatch and result handling
  • Permission checks before tool execution
  • Plugin hooks at each stage
  • Context compaction when the conversation grows

AI SDK Abstraction

The @opencode-ai/ai package provides a provider-neutral LLM layer:

import { Effect } from "effect"
import { LLM } from "@opencode-ai/ai"
import { OpenAI } from "@opencode-ai/ai/providers/openai"

const model = OpenAI.model("gpt-4.1-mini")

const program = Effect.gen(function* () {
  const result = yield* LLM.generate({
    model,
    system: "You are concise.",
    prompt: "Explain Effect in one sentence.",
  })
  console.log(result.text)
  console.log(result.usage)
  console.log(result.cost)
})

Key types:

  • LLM.request({...}) - build a provider-neutral request
  • LLM.generate / LLM.stream - execute requests
  • LLMEvent.is.* - typed guards for stream filtering (is.textDelta, is.toolCall, is.finish)
  • TestLLM - deterministic test client for scripting responses

Database Layer

V2 uses SQLite with Drizzle ORM. The database service lives in packages/core/src/database/. Session messages, events, and projections are persisted in SQLite tables managed by Drizzle migrations.

The V1-to-V2 migration runs through an experimental server endpoint. It performs per-session replacement in checkpointed transactions rather than whole-table deletes, to avoid blocking the running TUI.

Server Composition

The server assembles all core services into a single Layer in packages/server/src/routes.ts:

// Simplified from routes.ts
const serverLayer = Layer.mergeAll(
  Config.layer,
  Session.layer,
  Agent.layer,
  Tool.layer,
  Permission.layer,
  MCP.layer,
  PluginRuntime.layer,
  // ... plus 20+ other service layers
)

The HTTP router maps protocol groups (session, agent, tool, event, etc.) to handlers that call into core services.