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.gengenerators compose service dependencies viayield*- Services bind to named variables before calling methods (not nested yields)
Layer.effectconstructs the service from its dependencies
Core Services
| Service | Tag | Role |
|---|---|---|
Agent.Service | @opencode/Agent | Agent registry, resolution, selection |
Session | @opencode/Session | Session lifecycle, persistence, runner |
Tool | @opencode/Tool | Tool registration, snapshots, execution |
Permission | @opencode/Permission | Permission rules, evaluation |
Config | @opencode/Config | Config loading, merging, normalization |
Bus | @opencode/Bus | Internal event bus |
MCP | @opencode/MCP | MCP server management |
PluginRuntime | @opencode/PluginRuntime | Plugin 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 requestLLM.generate/LLM.stream- execute requestsLLMEvent.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.