Background on why it works the way it does. This is contributor/maintainer
material -- end users don't need any of it (see the top-level README.md
for install and usage). For what the contract is rather than why, see
docs/SPEC.md.
amplifier-opencode is a launcher and adapter, not a fork or a plugin. It
sits between two independently developed projects it does not modify: the
opencode TUI and the
amplifier-agent HTTP server.
The whole adapter is one binary (src/amplifier_app_opencode/cli.py):
there is no separate daemon, no background service beyond the
amplifier-agent server this tool itself spawns, and no persistent state of
its own (see spec/file-locations.md, "no config file for this tool's
own settings"). Every invocation is a fresh discovery-and-translation pass.
Every launch/prepare invocation runs the same five observable stages, in
this order:
1. CHECK is amplifier-agent's chat-completions server already running?
(an HTTP probe against the base URL, never a PID file)
2. START if not, spawn `amplifier-agent serve chat-completions` in the
background, with the user's --workspace / --host-config
3. DISCOVER GET /v1/models from amplifier-agent
4. WRITE materialise an opencode provider config from that response
(global opencode.jsonc/.json, or <project-dir>/opencode.json)
5. EXEC replace this process with opencode (`prepare` stops before this
stage; `launch` runs it)
Stage 2 and stage 5 are where the two most consequential implementation
choices live. Stage 2 spawns the server with start_new_session=True
specifically so it detaches from this process's session and survives stage
5's exec (see "Key decisions" below). Stage 5 uses os.execvp, replacing
this process's PID with opencode's rather than forking a child and waiting
on it, so opencode inherits the terminal directly and this tool is not on
the hook for forwarding signals (Ctrl-C, terminal resize) to a child it
would otherwise have to supervise.
opencode's documented contract for custom OpenAI-compatible providers (the
"Atomic Chat" pattern)
requires a static models block listing every model ID the upstream
server serves. opencode does not auto-fetch /v1/models at runtime for
custom-config providers.
Rather than maintain the model list by hand or attempt fragile runtime
monkey-patches against opencode's internals, this binary materialises the
static config from the live /v1/models response before opencode
launches. Every invocation re-syncs. The model list is therefore always
live; the "static" nature of opencode's config is just an implementation
detail this tool absorbs on the user's behalf.
A second, independent rationale governs which fields are allowed into the
generated block at all: opencode's per-model schema lenient-strips unknown
keys but fails the whole config decode on a type mismatch in a known
field, and when that decode fails, opencode's own loader catches the error
and silently substitutes an empty config, wiping every provider's models
block. This tool is therefore paranoid about types on the way out: a field
that cannot be emitted with the correct type is omitted entirely rather than
emitted with a placeholder, because a missing field is recoverable and a
decode failure is not. See spec/opencode-config.md for the exact
per-field rules this produces.
opencode itself is unmodified. The adapter lives entirely outside both upstream projects: no plugins, no patches, no npm packages, no JavaScript. Everything this tool does, it does from the outside, by writing files opencode already knows how to read and by talking to amplifier-agent over the same HTTP face any client would use.
amplifier-agent exposes two server-side resource kinds, skills and modes,
over GET /v1/skills and GET /v1/modes. Each is bridged into a different
opencode-native concept, because each maps onto something opencode already
has:
skill -> opencode slash command a skill is a one-shot, user-invoked action;
opencode's command files are exactly that
mode -> opencode primary agent a mode is a standing behavioral stance for
the whole session; opencode's agent
picker is exactly that
The mode directive travels in the generated agent file's body
([amplifier-agent:mode=<name>]), not in opencode's config, because
opencode forwards a primary agent's body to the backend as a system message
on every turn as part of its own existing behavior. That means mode
persistence across a session is free: nothing in this tool has to track
"which mode is this session in" separately, because opencode re-sends the
directive on every single turn as a side effect of a mechanism it already
has. Putting the same information in config instead would have required
this tool (or amplifier-agent) to read opencode's config back out at request
time, a channel that does not exist in the other direction. See
spec/skills-and-modes-bridge.md for the byte-exact file shapes and
the ownership-manifest reconciliation that keeps repeated runs idempotent.
This tool contains no model logic and no prompt content. There is no system prompt authored here, no tool-calling logic, no reasoning about what the user asked for. Every intelligent behavior a user experiences, the actual reply content, tool use, skill execution, mode behavior, happens server-side inside amplifier-agent. This tool's entire job is discovery (asking amplifier-agent what it can currently do) and translation (expressing that answer in a shape opencode's config and file-based extension points already understand). That split is deliberate: it is what lets amplifier-agent ship new skills, modes, or providers without this tool ever needing a code change to surface them.
- Regenerate the config every launch instead of patching it once.
Keeps the model list, and the skills/modes bridge, always in sync with
whatever amplifier-agent is currently serving, at the cost of the
provider.<provider-id>sub-block always being a full replace rather than a merge (seespec/opencode-config.md, Preservation). - Delegate all credential storage to amplifier-agent. This tool has no credentials file and no in-memory cache of a secret beyond a single subprocess call. One source of truth for what will actually resolve at serve time, and one less place a secret could leak from.
- Pin the self-heal target to an exact tag, never a moving branch. The
silent, launch-time auto-install of amplifier-agent targets
AGENT_PINNED_REF(a specificvX.Y.Ztag), so a reliability mechanism never drags a user onto un-vettedmain. - Two independent ownership manifests, not one shared state file. The skills bridge and the modes bridge each reconcile against their own manifest. A failure or a stale manifest in one face cannot corrupt the other's bookkeeping.
- Never stop the spawned server. The server is meant to outlive a
single
launch/execcycle so a later invocation can reuse it (and whatever session state it holds) instead of paying a cold start every time. This tool owns starting the server; stopping it is explicitly out of scope (seespec/cli.md, Non-goals). exec, not subprocess-wrap, for opencode itself. Signal handling and terminal control belong to opencode once it is running; a supervising wrapper process would have to reproduce that handling itself for no benefit.
docs/SPEC.md-- the contracts this architecture implementsdocs/E2E_TESTING.md-- how the whole thing is validated end to end, against the real opencode TUI and a real amplifier-agent serverISSUES.md-- tracked gaps between the stated contract and current behavior../AGENTS.md-- the process anchor for working on this repo