A data-oriented Clojure reimplementation of the OpenCode AI coding agent. Functional, REPL-driven, and data-first.
This project translates the TypeScript OpenCode agent into idiomatic Clojure, one module at a time. The architecture follows the Diplomat pattern — pure domain logic separated from side-effecting adapters, with Integrant managing the component lifecycle.
Built so far:
- Session 1 — Config system: Aero-based EDN config loading, Malli schema validation, Integrant lifecycle, CLI entry point with
tools.cli - Session 2 — Core data model: Message schemas and constructors, session manipulation functions, core.async pub/sub event bus
- Session 3 — LLM provider abstraction: Protocol definition, model metadata registry, SSE stream parsing utilities
- Session 4 — Anthropic provider: Full LLMProvider implementation with HTTP calls via hato, message format conversion, streaming via SSE/core.async, Integrant wiring
- Session 5 — Tool system: Tool framework with registry, multimethod dispatch, JSON Schema conversion via Malli. Three tool implementations:
read_file,write_file,glob - Session 6 — Complete tool suite:
edit_file(search/replace with exact + whitespace-normalized matching),bash(shell execution with timeout + truncation),grep(ripgrep/grep with output parsing + truncation) - Session 7 — Infrastructure for the agentic loop: UI adapter protocol + JLine REPL implementation, atom-backed session persistence, permission system (allow/ask rules with dangerous-mode override)
- Session 8 — The brain: System prompt construction and core agentic loop (stream → tool calls → loop until done, max 25 iterations guard, error handling, event publishing)
- Session 9 — MVP MILESTONE: Main entry point with interactive REPL loop, full Integrant system wiring, all tool namespaces loaded at startup. The application is now a working agentic coding assistant.
┌──────────────────────────────────────────────────────┐
│ system.clj │
│ Integrant wires: :opencode/config │
│ :opencode/event-bus │
│ :opencode/llm-provider (→ config) │
│ :opencode/ui │
│ :opencode/session-store │
└────────┬──────────────────────┬───────────────────────┘
│ │
config.clj logic/event_bus.clj
(Aero + Malli) (core.async pub/sub)
│
publish! / subscribe! / close-bus!
│
┌──────────────────────┼──────────────────┐
│ │ │
domain/message.clj domain/session.clj adapter/llm/
(Malli schemas, (pure transforms, ├── provider.clj (protocol)
constructors) token tracking) ├── anthropic.clj (impl)
└── model_registry.clj
domain/tool.clj adapter/tool/
(registry, schemas, ├── file_read.clj (read_file)
multimethod dispatch, ├── file_write.clj (write_file)
JSON Schema convert) ├── glob.clj (glob)
├── file_edit.clj (edit_file)
├── bash.clj (bash)
└── grep.clj (grep)
logic/ui.clj (UIAdapter protocol/port) adapter/persistence.clj
(atom-backed SessionStore)
adapter/ui/
└── repl.clj (JLine ReplUI impl)
logic/streaming.clj
(SSE parsing → core.async channels)
logic/permission.clj
(allow/ask rule table)
logic/prompt.clj
(system prompt builder)
logic/agent.clj
(core agentic loop)
- Domain layer (
opencode.domain.*) — Pure functions and Malli schemas. No I/O, no side effects. Messages are plain maps with namespaced keywords (:message/role,:session/id). Constructors validate via Malli and return anomaly maps on invalid data. - Adapter layer (
opencode.adapter.*) — Side-effecting code at the edges. TheLLMProviderprotocol defines the interface for LLM completions and streaming. The Anthropic adapter implements the protocol with real HTTP calls via hato and SSE stream parsing via core.async. The model registry stores static model metadata (context windows, costs, capabilities). Tool adapters (adapter/tool/) implementexecute-tool!multimethods for file I/O, glob, edit, bash, and grep operations. TheUIAdapterprotocol abstracts all user-facing I/O (display, permission prompts, input);ReplUIimplements it with JLine 3 and ANSI colors.SessionStoreprovides atom-backed persistence for session data. - Logic layer (
opencode.logic.*) — Orchestration with managed side effects. The event bus uses core.async channels with sliding buffers for pub/sub. SSE streaming utilities parse Anthropic server-sent events into channel-based event streams. The permission module checks tool rules (allow/ask) with a dangerous-mode override. - Config — EDN files read by Aero with
#envtag literals. Malli validates at startup; invalid config returns cognitect anomaly maps.
# Start the application
clj -M -m opencode.main
# With flags
clj -M -m opencode.main --dangerously-skip-permissions
# Start a development REPL
clj -M:dev# 1. Set your API key
cp .env.example .env
# Edit .env and add your ANTHROPIC_API_KEY
# 2. (Optional) Place project files in ./workspace/ for the agent to operate on
# 3. Start the interactive agent
docker compose up
# Run with dangerous mode (skip permission prompts)
docker compose run --rm opencode --dangerously-skip-permissions# Run the full test suite (Kaocha)
clj -M:test
# Lint with clj-kondo (must be zero errors)
clj-kondo --lint src test- Updated
opencode.main— Interactive REPL loop:- Starts Integrant system, registers shutdown hook for clean teardown
- Creates initial session, displays welcome message with model name
- Main loop: reads input via
ui/get-input!, dispatches to agent loop for LLM processing - Special commands:
/quit,/exit,/new(new session),/sessions(list sessions) - Saves updated sessions to persistence store after each agent loop iteration
- All user output goes through UIAdapter (AGENTS.md compliance — no direct println in business logic)
- Updated
opencode.system— Full Integrant wiring:- All 6 tool adapter namespaces required at startup (bash, file_edit, file_read, file_write, glob, grep)
- Ensures defmethod registrations execute before first tool call
- 113 tests, 283 assertions, 0 failures. clj-kondo: 0 errors, 0 warnings.