| title | OpenCode Architecture and System Design | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| owner | OpenCode team | ||||||||||||||||||||||
| status | production | ||||||||||||||||||||||
| last_verified | 2026-09-13 | ||||||||||||||||||||||
| reproduce |
|
Status: production Last Updated: 2026-09-06 See also:
prompt_kernel/README.md— kernel gate graph, serialization order, install pipelinedocs/compaction.md— mechanistic compaction (stable continuous memory)
Production system kernel: prompt_kernel/source.py → packages/opencode/src/session/prompt/reasoning_prompt.txt (python -m prompt_kernel --install).
-
packages/opencode/src/provider/provider-sync.ts/PROVIDER_SOURCES- Input: canonical
streamlake-vanchinprovider ID. - Output: a first-class Pay-as-you-go OpenAI-compatible provider at
https://vanchin.streamlake.ai/api/gateway/v1/endpoints, authenticated throughSTREAMLAKE_API_KEY. - Logic: this static source supplies no account-specific endpoint ID, so the provider chooser can render the provider without fabricating a model.
- Input: canonical
-
dialog-streamlake-vanchin-state.ts/STREAMLAKE_VANCHIN_MODELS- Input: the official Vanchin model-list snapshot and one endpoint ID.
- Output: a model profile containing documented context/output bounds, reasoning, Function Call, and text/image/video input modalities.
- Logic: only text and multimodal chat profiles are selectable. Gateway
GETmetadata paths returned HTTP 400 with an authenticated key, so the profile comes from the official catalog while the endpoint ID comes from the Vanchin console. Reasoning profiles add the vendor-documentedenable_thinking: trueoption to the request body.
-
dialog-provider.tsx/connectStreamLakeVanchin- Input: API key,
ep-…inference endpoint ID, and deployed profile selected by the user. - Output: an auth-store credential plus a minimal RFC 7386
/configpatch, then the ordinaryDialogModelpicker. - Logic: the API key is never included in the configuration patch; the selected profile, rather than free-form modality input, determines model capabilities. The Chinese OpenAI protocol reserves wire-level
modalitiesfor Qwen-Omni audio output; normal image/video-understanding profiles do not send it.
- Input: API key,
-
provider/transform.ts/providerOptions- Input: a Vanchin Pay-as-you-go OpenAI-compatible request.
- Output: provider options containing the selected profile’s
enable_thinkingsetting and no legacy coding-template field. - Logic: the old template option is now limited to a
/api/gateway/coding/v1URL and cannot leak into the Pay-as-you-go endpoint.
Oracle: bun test test/tui/dialog-streamlake-vanchin-state.test.ts and bun test test/provider/transform.test.ts --test-name-pattern "keeps Vanchin PayGo requests free".
packages/opencode/src/provider/provider-sync.ts / PROVIDER_SOURCES (huggingface):
- Input:
https://router.huggingface.co/v1/models(HF Inference Providers chat router; anonymous listing works,HF_TOKENis sent when present). - Logic: the router list discloses less metadata than the curated registry (no reasoning/interleaved/description/output-limit), so this source merges instead of replacing — curated fields survive, live pricing/context/modalities win, upstream-only ids are retained (models.dev
deleteMissing:falsesemantics), and new ids are added. Quantisation variants (-BF16,-FP8, …) inherit capability metadata from their base model. Pricing collapses the router's per-provider table to the default:fastestroute (routed provider, else the fastest priced one) — already USD per million tokens. - Oracle:
bun test test/provider/provider-sync.test.ts; targeted refresh:bun run script/provider-sync.ts(packages/opencode).
┌─────────────────────────────────────────────────────────────┐
│ PROMPT ASSEMBLY │
│ │
│ prompt.ts: runLoop() │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. Checkpoint.load(sessionID, model) │ │
│ │ ├── HIT → reuse systemPrompt + messages │ │
│ │ └── MISS → Effect.all([skills, env, instructions, │ │
│ │ rules]) → assemble fresh │ │
│ │ │ │
│ │ 2. system = [banner, ...rules, ...env, ...skills, │ │
│ │ ...instructions] │ │
│ │ │ │
│ │ 3. modelMsgs = toModelMessagesEffect(msgs, model) │ │
│ │ │ │
│ │ 4. llm.stream(system, modelMsgs, tools) │ │
│ │ ├── Provider prefix cache: [session: key] │ │
│ │ ├── plugin.transform hook (mutates system in-place) │ │
│ │ └── Cache collapse: header → second → tail │ │
│ │ │ │
│ │ 5. Success → Checkpoint.save(system + modelMsgs) │ │
│ │ Failure → checkpoint untouched (rollback) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ System prompt order: │
│ [immutable rules/env/skills/instructions] [session banner] │
│ banner is the final mutable cache fence │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ CHECKPOINT SYSTEM │
│ │
│ Storage: {data}/log/.checkpoints/ │
│ {provider}_{model}_{agent}_{sessionID}_S{0|1}.enc │
│ Memory: process map published on save (no disk race) │
│ │
│ Encryption: AES-256-GCM │
│ Key: SHA-256(projectID:sessionID:salt) │
│ Atomic: write .tmp → rename → .enc (2-slot rotate) │
│ │
│ CheckpointData v4 { │
│ systemPrompt[] path system (frozen until compact)│
│ identityFingerprint kernel+agent prompt only │
│ messages[] + messageIDs[] + messageFingerprints[] │
│ model, agent, turn, timestamp │
│ } │
│ │
│ Policy (KV continuous, multi-project stable): │
│ • Path system (AGENTS.md/skills/rules) frozen mid-era │
│ • Refresh only on compact OR identity fingerprint break │
│ • Per model (+ agent) slots — switch models, nothing lost │
│ • Message reuse: ordered prefix + content fingerprints │
│ (in-place edits re-convert from first dirty message) │
│ │
│ Diffs: request-diff remembers last formatted request so │
│ post-compact turns still produce a diagnostic .diff │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ AGENT SYSTEM │
│ │
│ Primary agents (user-facing): │
│ build — full-access development (provider prompt) │
│ plan — read-only planning (denies edits) │
│ │
│ Subagents (invoked via task tool or pipeline): │
│ ┌──────────┬──────────────┬────────────────────────────┐ │
│ │ general │ explore │ coder │ researcher │ │
│ │ planning │ file search │ implement │ gather info │ │
│ │ strategy │ code grep │ edit/write │ read-only │ │
│ ├──────────┼──────────────┼────────────┼───────────────┤ │
│ │ media │ title │ │ │ │
│ │ generate │ name │ │ │ │
│ │ images │ sessions │ │ │ │
│ └──────────┴──────────────┴────────────┴───────────────┘ │
│ (No separate compaction or summary agent — summarization runs via
│ /summarize + SessionSummary; see § Mechanistic Compaction) │
│ │
│ Pipeline tool: chains subagents sequentially │
│ researcher → coder: gather evidence → implement │
│ explore → general: find files → plan approach │
│ media → researcher: generate → verify │
│ │
│ Tools (built-in): │
│ capability, pipeline, task, bash, edit, write, read, │
│ glob, grep, list, webfetch, universalsearch, │
│ messagesearch, sessionread, joboutput, jobwait, jobkill, │
│ todowrite, question, skill, lsp, planexit, fossilgrep, │
│ codegraph, invalid │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MEDIA SYSTEM │
│ │
│ Attachment Pipeline: │
│ User upload → Handler.classify() → UniversalAttachment │
│ → Handler.enrich() → metadata extraction │
│ → Handler.normalize() → resize/optimize │
│ → toModelMessagesEffect() → provider format │
│ │
│ 14 Handlers: │
│ audio, video, image, document, binary, text, code, │
│ sensor, spatial, archive, data, spreadsheet, │
│ presentation, image_vector │
│ │
│ Terminal TUI Media (Module C): │
│ MediaImage ← chafa --format symbols (inline terminal) │
│ MediaVideo ← ffmpeg thumbnail + mpv --vo=gpu (external) │
│ MediaAudio ← mpv --vo=null (terminal playback) │
│ │
│ Multimodal Messages (Module D): │
│ • case "file" in processor.ts → FilePart creation │
│ • Output modalities in system prompt │
│ • FilePart rendering in assistant messages │
│ • toModelMessagesEffect includes assistant media │
│ │
│ Capability Tool: │
│ Queries models_capabilities.yaml → cross-refs auth.json │
│ → returns ranked models by modality + API key status │
└─────────────────────────────────────────────────────────────┘
Canonical (code Exact): docs/compaction.md · session-memory-graph.md
Do not summarize this section from memory — the control-flow traps are real:
stop path: finishStep → Checkpoint M → maybeCaptureSidecar? → break
^^^ no compact() here
in-band compact: only if loop does NOT break first (e.g. tool-continue)
AND needsContentCompaction(open ≥ 65_536)
emergency: processor needsCompaction → compact()
injectSummaryRequest: implemented, NOT called from prompt.ts
| Piece | Tokens | When |
|---|---|---|
| Sidecar | ephemeral LLM | stop + thresholds |
| compact() | zero | emergency, or in-band if loop continues |
| Safety size | content/4 + 10k | fit / usable (not cadence) |
Model = Inferred prose. System = Exact IDs / diffs / materialize / soft-hide.
┌─────────────────────────────────────────────────────────────┐
│ CACHE & DIFF SYSTEM │
│ │
│ provider/transform.ts: │
│ • Anthropic-dialect `cache_control: ephemeral` markers │
│ on the last cacheable content block │
│ • md5 of the system parts feeds the prompt-cache key │
│ • the speculative fingerprint audit was removed in │
│ 352e073279 — cache:broken fired on false positives │
│ │
│ request-diff.ts: │
│ • Encrypted baseline (.enc files in .baselines/) │
│ • AES-256-GCM, key = SHA-256(project:worktree:sid:salt) │
│ • Section-aware diff: META + SYSTEM + MESSAGES │
│ • alignLines: fast-path prefix scan (O(N) for identical │
│ system prompts, avoids O(N²) sync-point search) │
│ • Written every turn (fire-and-forget), survives restarts │
│ │
│ checkpoint.ts: │
│ • Reuses encryption + baselinePath from request-diff │
│ • Stores structured prompt (not debug-formatted text) │
│ • kind: "checkpoint" distinguishes from diff baselines │
│ • Atomic write: tmp → rename → .enc │
│ │
│ OpenRouter affinity (llm.ts + provider/transform.ts): │
│ • banner == body session_id == header x-session-id │
│ == prompt_cache_key (final provider cache namespace) │
│ • a subagent uses its reusable task-cache lease, not its │
│ physical child session ID │
│ • provider.only filters a single upstream without order │
└─────────────────────────────────────────────────────────────┘
┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐
│ User │───▶│ prompt.ts │───▶│ llm.ts │───▶│ Provider │
│ Input │ │ assemble │ │ reorganize │ │ API │
└──────────┘ └──────┬───────┘ └──────┬──────┘ └────┬─────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌────▼─────┐
│ Checkpoint │ │ Plugin │ │ Response │
│ Load/Save │ │ Hook │ │ Stream │
└─────┬─────┘ └───────────┘ └────┬─────┘
│ │
┌─────▼─────┐ ┌────▼─────┐
│ .enc │ │Processor │
│ Files │ │ Events │
└───────────┘ └────┬─────┘
│
┌───────────────────────────────────┤
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ Diff │ │ Session │
│ Logger │ │ DB │
│ .enc │ │ SQLite │
│ .diff │ └───────────┘
└───────────┘
See docs/agi-workflow.md. Runtime util/plan-status.ts standardizes plans/ vs plans_completed/ by checkbox presence; AGI only terminates when hygiene is clean.
session/llm.ts builds ONE correlation header set for EVERY provider — no
per-provider branching:
x-opencode-session/x-opencode-request/x-opencode-project/x-opencode-client— opencode correlation; Zen reads these for routing/telemetry.x-request-id(= message id) — fills provider dashboards' "Request ID" columns (e.g. Novita) and our gateway metrics.x-session-id+x-session-affinity— unified affinity namespace:providerCacheKeyfor openrouter,sessionIDotherwise. OpenRouter's formerX-Session-Idmerged here (commit 8e5bdc41).x-parent-session-id— set when subagent parent exists.X-OpenRouter-Cache/X-OpenRouter-Cache-TTL— strict opt-in viaprovider.<id>.options.responseCache(+responseCacheTtl, 1–86400s; seeprovider/response-cache.ts). Response caching only — NOT prompt caching.User-Agent: opencode/<version>.
Wire policy: transports send headers VERBATIM. Gateway h1/h2 transports do
not filter anything (user directive 2026-09-07; the earlier allowlist was
superseded). Only adaptive-client removes consumed OAuth inputs
(x-opencode-oauth-token → Authorization, x-opencode-account-id →
ChatGPT-Account-Id, x-opencode-oauth-url → URL rewrite) so raw credentials
never duplicate into provider logs. Local diagnostics (wireHeaders,
sanitizeHeaders) still redact auth from gateway log files.
Anthropic subscription OAuth keeps its provider auth hook. The hook prepares the
Messages request and passes it to __gatewayFetch when the gateway is active;
without an active gateway it uses fetch. Its final body is a UTF-8 string so
the gateway can classify streaming requests and send the complete body through
the selected transport. The hook preserves the max_tokens value selected by
the model layer; subscription OAuth does not impose a second 64k ceiling. The local wire test in
test/plugin/anthropic-auth.test.ts covers HTTP/1.1 without credentials;
Anthropic HTTP/2 and HTTP/3 still need separate transport probes. OAuth login
and token refresh remain in the auth hook, outside the gateway transport.
/agents → ctrl+o opens a model-aware routing editor. It fetches the selected
model's live OpenRouter endpoints, offers a radio group for dynamic price,
throughput, latency, or OpenRouter-default routing, and exposes checkbox
controls for priority order, strict only, quantization, and fallback policy.
The initial focus is the first sort choice; arrows skip section headings, and
Space, Enter, or a mouse release activate the same row. Selecting a provider
switches to manual policy; selecting any dynamic option, including the default,
clears the manual provider list.
The form displays effective inherited routing but labels the write destination
explicitly. Its Save row is the sole write action; it does not open a second
confirmation dialog. In global /agents model editing, model and variant are
staged together and committed by one Save and one global-config update. If that
save targets the active agent, the current session adopts the same model and
variant for its next prompt, overriding older worktree state; an already running
request stays pinned to the model recorded when it began.
/agents exposes Sampling parameters (ctrl+g) for every agent's resolved model.
The form initializes the balanced thinking-agent profile: temperature: 0.65,
repetition_penalty: 1.1, top_p: 0.95, and presence_penalty: 0.2; each numeric field is editable and
the final row is the only write action. Session overrides in
sessions/{sessionID}.jsonc take precedence over worktree model.json, then
over provider.<id>.models.<id>.sampling configuration. The request maps
temperature/top-p/presence to the AI SDK's top-level parameters and routes
repetition_penalty through the existing provider option transformation.
The OpenRouter endpoint form retains a permanent one-line endpoint-status region while live data arrives. Its headers no longer change between loading and loaded states, avoiding transient text flashes and vertical form jumps.
Owner, 2026-10-13: «в кернеле критический баг. Он позволяет не доделывать работу». The mechanism
half ships on this host (session/stop-check.ts); the kernel text is a separate task (t37). At the
runLoop exit (session/prompt.ts, the exiting loop branch — a completed turn: finish set, no
pending tool call) a primary-agent turn may not END on a prose question/offer to the owner
(«Хотите, поищу…?» / «Want me to…?») or by handing him an action («запусти …» / «you need to run …»):
the decision function fires two detectors — the same ones as the Claude host's Stop hook
no_offer_stop.py (trailing question after the SV block is stripped; the hand-off regex after
fenced code and quoted «…»/"…"/`…` spans are dropped, so a citation is not a hand-off) —
and the runtime continues the turn ONCE with a synthetic user message carrying the reason
(metadata.stop_check is the latch, so it can never fire twice in a row, restarts included).
A turn that called the question tool is never blocked; sub-agent turns are never blocked;
OPENCODE_STOP_CHECK=0|false|off|no disables it. The injection is a tail message — no byte of the
system prefix changes (KV).
2026-10-14 — the stop-claim form (owner «Да + механизм», t42 report §1.5/§2). A blocked/stopped
outcome is a claim about the whole route space and cannot be verified, only scoped: routes tried,
each with a receipt and the failing component; untried routes named; reopen_when. Kernel text binds
nothing at runtime (experiments/2026-10-13_kernel-exit-audit/PREDICATES.md: the FSM predicates have
0 runtime consumers), so the form is carried by two mechanisms over one predicate
(util/stop-claim.ts):
util/plan-status.tsaudits every plan underplans/postponed/and every plan underplans/whose stated lifecycle is BLOCKED/WAITING/POSTPONED — a missing or incomplete claim is reported as aSTOP CLAIM: N plan(s) …violation that NAMES each missing piece (theNo checklistcontract, never a silent accept). The 8 plans parked before this change are the red set the check exists to surface; fixing them is their owners' decision, not the audit's.session/stop-check.tsadds a third detector: a primary-agent turn whose LAST paragraph declares a blocked/stopped outcome (BLOCKED, «заблокировано», «не могу продолжить», postponed) and carries no valid claim is continued ONCE withSTOP_CLAIM_REASON— the same latch, never twice in a row.
| File | Purpose | Lines |
|---|---|---|
session/prompt.ts |
Main prompt loop, checkpoint, captureSidecar, Layer-2 cadence gate |
— |
util/plan-status.ts |
Plan progress + reconcilePlans hygiene; the STOP CLAIM violation for parked/blocked plans |
— |
util/stop-claim.ts |
The ## Stop claim form: section finder, problems, validity — one home for both surfaces |
— |
cli/cmd/tui/context/agi-mode.tsx |
AGI loop, plan hygiene integration | — |
session/llm.ts |
LLM orchestration; request size ≈ content/4+10k; correlation headers (§8a) | — |
provider/response-cache.ts |
OpenRouter response-cache opt-in headers (§8a) | — |
provider/balance.ts |
Model status registry: deepseek/openrouter/novita-ai balance, usage windows | — |
session/system.ts |
Environment, capabilities, provider prompts | — |
session/instruction.ts |
AGENTS.md/rules loading, caching | — |
session/checkpoint.ts |
Per-model encrypted checkpoint save/load | — |
session/incremental-checkpoint.ts |
Sidecar project_checkpoint CRUD / materialize |
— |
provider/transform.ts |
Anthropic cache_control breakpoints, prompt-cache key |
— |
session/request-diff.ts |
Encrypted baseline, diff engine | — |
session/compaction.ts |
compact() + legacy summary helpers |
— |
session/overflow.ts |
Cadence vs safety gates; needsContentCompaction |
— |
sync/index.ts |
SyncEvent.run / runBatch (finishStep one TX) |
— |
session/message-v2.ts |
Schema, conversion, filterCompacted* |
— |
session/stop-check.ts |
k2 end-of-turn stop check: offer/hand-off/stop-claim detectors, decision, latch key | — |
agent/agent.ts |
10 built-in agent definitions | 395 |
tool/pipeline.ts |
Pipeline tool (agent chaining) | 191 |
tool/capability.ts |
Model capability lookup tool | 101 |
tool/registry.ts |
23 built-in tool registry | 376 |
attachment/handlers/ |
14 media type handlers | ~800 |
cli/cmd/tui/component/ |
Terminal TUI components (31 files) | ~3000 |
(links inside the moved text use repo-root-relative paths, as they were written in AGENTS.md)
| Layer | Mechanism | Kills |
|---|---|---|
| 1. Task geometry | Fractal lattice + cosine filter + k-medoids → CENTRAL_TASKS |
Monolith plans; goal drift |
| 2. Prior art | universalsearch web/code/hybrid; local codegraph |
Reinvention; guesswork |
| 3. Oracles | Baseline [Exact] before edit; post-impl pass before [x] |
"Works on vibes" |
| 4. Conversation memory | Mechanistic compaction → message*; never delete |
Memory soup |
| 5. Working-copy memory | Fossil snapshots + CodeGraph folder-scoped impact | "What did we change?" |
| 6. Direction lock | SV / decisions preserved; re-cluster against original goal | Silent mission creep |
Owner, verbatim: «любая тема должна быть экономически целесообразной и достаточно замкнутой чтобы не трахать мозги обслуживающему персоналу», and on the statement below: «обвести в рамку и повесить на стенку».
Closure and safety are not in tension — they are the same property. A loop that proves its own results and cannot leave its envelope needs neither a supervisor nor a prompt engineer standing next to it. What industry buys is not a clever model but a stated price per verified task and near-zero human touches.
Safety here is MECHANICS, not warnings: the envelope, an oracle per task, rollback, one store arbiter. A loop is judged by three measured numbers per verified task — Claude/verifier tokens, robot $, and owner touches (answers, restarts, manual fixes); the last one → 0 for the routine class is what «closed» means. Sizing and blockers: plans/2026-09-30_robot-delegation-and-orchestration.md § Scaling.
| Agent | Mode | Prompt | Description |
|---|---|---|---|
build |
primary | provider family prompt | Default full-access development agent |
plan |
primary | provider family prompt | Read-only planning (denies edits) |
orchestrator |
primary | prompt/orchestrator.txt |
Autonomous orchestrator for AGI mode |
general |
subagent | prompt/general.txt |
Planning, design, root-cause analysis |
explore |
subagent | prompt/explore.txt |
Fast file/code/conversation search |
coder |
subagent | prompt/coder.txt |
Code implementation (edit/write/bash) |
researcher |
subagent | prompt/researcher.txt |
Read-only research (code+web+history) |
media |
subagent | prompt/media.txt |
Media generation via capability tool |
title |
primary (hidden) | prompt/title.txt |
Session title generation |
Tools: pipeline chains subagents sequentially. capability looks up model
modalities. compact arms a boundary fold of the session window — primaries
only (see Mechanistic Compaction § three triggers).
Per-identity sampling. Each native subagent declares its own
temperature / topP / options.repetition_penalty in
src/agent/agent.ts — verification identities (explorer, coder) are
sampled tight and unpenalised so their output is reproducible and their
repeated tokens (paths, identifiers) survive; generative ones (general,
media) are sampled loose so a candidate set actually differs. Model-wide
sampling merges before the agent in session/llm.ts, so the narrower
declaration wins. presence_penalty/frequency_penalty are NOT sendable:
the vendor documents both as deprecated no-ops and its endpoint rejects them
beside repetition_penalty with a 400, so the pair was removed from the whole
assembly on 2026-09-30 (plans/2026-09-30_drop-penalty-sampling-params.md) —
upstream opencode sends neither. Only what the provider honors binds — see the vendor
reasoning contract below.