opencodeacp

package module
v0.0.0-...-849c623 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 39 Imported by: 0

README

acp-go-opencodev2

acp-go-opencodev2 exposes OpenCode v2 as an Agent Client Protocol agent. One opencode serve process handles the Agent's sessions through authenticated loopback HTTP and a shared event stream.

Sessions retain OpenCode's native storage. After closing the adapter, continue a session in the same directory and native home:

opencode run --session NATIVE_SESSION_ID "Continue the task"

New, load, and resume responses and session-list entries expose the current native ID as _meta.opencode.nativeSessionId. Use it for native CLI continuation. ACP requests continue to use the stable ACP sessionId. The store's configuration record saves both IDs with the matching native history.

Install and run

go install github.com/savid/acp-go-opencodev2/cmd/acp-go-opencodev2@latest
acp-go-opencodev2 [-path opencode] [-home DIR] [-scratch-dir DIR] [-model provider/id] [-seed-file rel=host]... [-debug]

The default executable is opencode; select an alternate local installation with -path opencode2. A bare -path is resolved on the inherited PATH. -home maps DIR/data, DIR/config, DIR/cache, and DIR/state to the four XDG home variables; omit it to use native home resolution. Native CLI continuation uses those same variables when a home was supplied. -seed-file writes a relative file inside OpenCode's configuration directory. -scratch-dir holds the temporary environment plugin. -version prints the adapter version. Standard OTEL_* variables configure telemetry exporters.

Embed

err := opencodeacp.Serve(ctx, os.Stdin, os.Stdout,
    opencodeacp.WithHome("/srv/opencode"),
    opencodeacp.WithSessionStore(store),
)

Options: WithExecutablePath, WithHome, WithScratchDir, WithInputHandoffRoot, WithDefaultModel, WithConfiguredModels, WithEnv, WithSeedFiles, WithSessionStore, WithConcurrencyLimits, WithImageLimits, WithLogger, WithTracerProvider, WithMeterProvider, WithTextMapPropagator, WithAgentName, WithAgentTitle, WithAgentVersion.

Session options

Pass _meta.opencode.options on new, load, or resume, or use WithSessionOpenCodeOptions from Go.

Field Meaning
model Provider-qualified model ID
mode Native agent ID, such as build or plan
effort Native model variant, forwarded unchanged
permission Native tool policy: ask, allow, or deny; default ask
env Environment overlay for tools in this session
extraPathDirs Absolute directories prepended to the session's PATH in order

The environment plugin reads the addressed session's metadata through OpenCode's API. Child sessions inherit that carrier through their native parent. OpenCode keeps its native authentication and configuration. Nonempty mcpServers and unknown owned options are invalid parameters.

session/set_config_option accepts nonempty model, mode, and effort values. The native provider catalog supplies model names, image capabilities, context windows, and available variants. Configured and selected models are included even when absent from the catalog. OpenCode v2 has no schema-enforced prompt API: outputSchema is rejected and no structured-output helper or capability is exposed.

Native permission requests use ACP permissions; native questions use ACP form elicitation. Missing or cancelled answers reject the native request. Forms requiring hidden, conditional, or external fields are cancelled. Unbound child sessions are mirrored, but their permission and form requests are refused. Commands come from OpenCode's command catalog. An exact /name match against an advertised command uses the native command endpoint; other text uses the prompt endpoint.

Images enter as inline base64 or validated file handoffs. Text must precede images because the native API separates the text from its attachments; other orders are rejected before dispatch. Output supports native file content and tool attachments, with bounded local reads and image limits. Remote URLs become resource links. _meta.opencode.rawEvent.enabled enables _opencode/rawEvent; image bytes are omitted from that diagnostic channel. Optional lifecycle negotiation supplies ordered session and turn updates.

Each model call reports a usage_update with its token breakdown when its session.step.ended event arrives, including the native session’s cumulative cost in USD. Agent message and thought chunks carry no messageId, and the breakdown carries no responseId: OpenCode keeps none of the gateway's response ids, and its own message and part ids are not response ids.

Persistence and runtime

SessionStoreFormat is opencode-session-export-v1. The main subpath holds native session exports for one conversation and its descendants. The config sidecar holds accepted options, captured local image bytes, and deferred synthetic inbox entries. A verified native snapshot commits atomically before terminal idle and the prompt response, including on cancellation. The execution’s terminal event identifies its durable idle marker in the native export. The root snapshot ends at that marker; each descendant ends at its latest idle marker, or has empty history if it has not completed an execution. A second graph read verifies the retained messages, configuration, and deferred inbox while later execution continues. Native cumulative usage and session timestamps retain their values at capture time and may include later execution. Commands without execution, configuration changes, restore, and close require matching idle graph snapshots. Synthetic reminders are restored with their native IDs without starting execution. A directory change returns backpressure while native inbox entries remain pending.

Load imports missing sessions through the native import API and replays ACP history. Resume imports without replay. Existing native messages must preserve every saved message’s identity, content, and order. Newly completed assistant, shell, and compaction records omitted by earlier native exports may appear between saved messages; later turns are adopted. Conflicting or shorter native histories are refused because the import API cannot replace a session. Local image replay remains available after its original file is removed. The default store is in memory; supply a durable store to restore across adapter restarts.

Close releases one session while peers retain the shared server. A server crash fails affected work, and the next operation starts a replacement and rebinds the addressed session. Delete tombstones the store entry. Native state remains available to OpenCode's CLI. A native-home file lock prevents two adapter servers from owning the same home concurrently.

Development

make test
make lint
make audit
make test-integration-smoke
ACP_GO_OPENCODEV2_MODEL=provider/model make test-integration-live

Unit tests use a scripted native HTTP server inside the test binary and require no installed OpenCode or credentials. Smoke tests use the installed CLI and a local stub provider without spending model tokens. Live tests use temporary homes and credentials supplied through the native environment (for example OPENCODE_API_KEY); they spend tokens. Set ACP_GO_OPENCODEV2_HARNESS_PATH=opencode2 to test an alternate installation.

Account usage

AccountUsageMethod (_opencode/accountUsage) accepts sessionId and providerId (opencode-go, openrouter, anthropic, or openai-codex). Initialization advertises the method, session scope, and supported providers. A provider routed through an explicitly configured native endpoint that publishes a gateway usage report is read with its effective catalog credential. Reads hold the session's foreground gate and spend no model tokens.

The adapter resolves the directory's effective API key and route through the native server. Only official endpoints and verified API-key routes are read; OAuth connections and unverified authentication overrides yield not_reported. OpenCode Console organization-scoped inference routes currently return not_reported; their account scope does not match the OpenCode Go key reader. Credentials stay local. Provider HTTP reads come from github.com/savid/acp-go-core/usage.

OpenCode Go reports rolling, weekly, and monthly percentage windows. OpenRouter reports key spending caps, lifetime spend, free-model request counts, and any account credit balance accessible with the same key. Dollar amounts are USD; a missing cap is explicitly uncapped, zero is a real value, and a negative remaining balance is preserved. Account credits and key caps remain separate. Each measurement retains its own observation and expiry times. Unavailable optional account credits do not discard key data.

Claude and ChatGPT subscription usage require effective OAuth credentials from the native runtime. Direct OAuth usage reads are not implemented; subscription measurements are available only through a verified configured gateway’s usage report.

Documentation

Overview

Package opencodeacp exposes OpenCode as an Agent Client Protocol agent.

Serve starts one authenticated loopback OpenCode server for all sessions. The harness inherits the adapter environment and keeps its native state in its XDG home, so the conversation can also continue through OpenCode's CLI.

WithSessionStore supplies durable per-conversation snapshots. Load restores and replays history; resume restores without replay. The adapter preserves native state when sessions close.

Hosts supply telemetry providers through WithTracerProvider and WithMeterProvider; the package never configures global providers.

Index

Examples

Constants

View Source
const (
	// RawEventMethod is the notification carrying one raw opencode event when a
	// session opted in through _meta.opencode.rawEvent.enabled.
	RawEventMethod = "_opencode/rawEvent"
	// AccountUsageMethod reads the addressed native provider's account usage.
	AccountUsageMethod = "_opencode/accountUsage"
	// SessionStoreFormat identifies the store layout this package writes: the
	// native export graph under main plus the adapter's session record
	// under the config subpath.
	SessionStoreFormat = "opencode-session-export-v1"
)

Variables

This section is empty.

Functions

func Serve

func Serve(ctx context.Context, input io.Reader, output io.Writer, opts ...Option) (returnErr error)

Serve runs an ACP agent over the provided streams. It blocks until the context is cancelled or the peer closes the connection, then closes the agent.

Example (Initialize)

ExampleServe_initialize embeds the agent over a pair of pipes, the same wiring a host uses for stdio, and reads the capabilities the handshake advertises.

package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"

	opencodeacp "github.com/savid/acp-go-opencodev2"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	clientToAgentReader, clientToAgentWriter := io.Pipe()
	agentToClientReader, agentToClientWriter := io.Pipe()

	done := make(chan error, 1)

	go func() {
		done <- opencodeacp.Serve(ctx, clientToAgentReader, agentToClientWriter)
	}()

	_, _ = fmt.Fprintln(clientToAgentWriter,
		`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}`)

	line, _ := bufio.NewReader(agentToClientReader).ReadString('\n')

	cancel()
	_ = clientToAgentWriter.Close()
	<-done

	var response struct {
		Result struct {
			AuthMethods       []any `json:"authMethods"`
			AgentCapabilities struct {
				LoadSession         bool           `json:"loadSession"`
				SessionCapabilities map[string]any `json:"sessionCapabilities"`
			} `json:"agentCapabilities"`
		} `json:"result"`
	}

	_ = json.Unmarshal([]byte(line), &response)

	fmt.Println(len(response.Result.AuthMethods))
	fmt.Println(response.Result.AgentCapabilities.LoadSession)
	fmt.Println(len(response.Result.AgentCapabilities.SessionCapabilities))
}
Output:
0
true
5

func SetModelRequest

func SetModelRequest(sessionID acp.SessionId, model string) acp.SetSessionConfigOptionRequest

SetModelRequest constructs a model selector update as "provider/id".

func ValidateOpenCodeSessionMeta

func ValidateOpenCodeSessionMeta(meta map[string]any) error

ValidateOpenCodeSessionMeta runs the owned-namespace parsing of a session lifecycle request's _meta without an Agent and returns the same refusal.

func WithSessionOpenCodeOptions

func WithSessionOpenCodeOptions(options OpenCodeOptions) wire.SessionRequestOption

WithSessionOpenCodeOptions merges opencode-specific options into _meta.opencode.options.

func WithSessionRawEvents

func WithSessionRawEvents(enabled bool) wire.SessionRequestOption

WithSessionRawEvents toggles raw opencode event emission for the session.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent exposes the opencode coding agent through ACP.

func NewAgent

func NewAgent(opts ...Option) *Agent

NewAgent creates an ACP agent for the opencode coding agent CLI. Construction never fails; a refused option is reported by Initialize and every session-establishing method as opencode_invalid_options.

func (*Agent) Authenticate

func (a *Agent) Authenticate(_ context.Context, params acp.AuthenticateRequest) (acp.AuthenticateResponse, error)

Authenticate exists because the SDK interface requires it. The harness authenticates itself in its own home, outside ACP.

func (*Agent) Cancel

func (a *Agent) Cancel(ctx context.Context, params acp.CancelNotification) (err error)

Cancel interrupts the session's in-flight turn. It is wire-silent on an unknown session or with no turn in flight.

func (*Agent) Close

func (a *Agent) Close() error

Close runs the shutdown ladder for every session and refuses every later request.

func (*Agent) CloseSession

func (a *Agent) CloseSession(ctx context.Context, params acp.CloseSessionRequest) (resp acp.CloseSessionResponse, err error)

CloseSession runs the shutdown ladder for one session.

func (*Agent) HandleExtensionMethod

func (a *Agent) HandleExtensionMethod(ctx context.Context, method string, params json.RawMessage) (any, error)

HandleExtensionMethod serves the account-usage read; every other extension method is method-not-found.

func (*Agent) Initialize

func (a *Agent) Initialize(ctx context.Context, params acp.InitializeRequest) (resp acp.InitializeResponse, err error)

Initialize implements ACP initialize.

func (*Agent) ListSessions

func (a *Agent) ListSessions(ctx context.Context, params acp.ListSessionsRequest) (resp acp.ListSessionsResponse, err error)

ListSessions lists live sessions and stored sessions, newest first.

func (*Agent) LoadSession

func (a *Agent) LoadSession(ctx context.Context, params acp.LoadSessionRequest) (resp acp.LoadSessionResponse, err error)

LoadSession restores a session and replays its history.

func (*Agent) Logout

func (a *Agent) Logout(_ context.Context, params acp.LogoutRequest) (acp.LogoutResponse, error)

Logout exists because the SDK interface requires it.

func (*Agent) NewSession

func (a *Agent) NewSession(ctx context.Context, params acp.NewSessionRequest) (resp acp.NewSessionResponse, err error)

NewSession creates and starts a opencode session.

func (*Agent) Prompt

func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (resp acp.PromptResponse, err error)

Prompt sends one turn to opencode and streams updates until it settles.

func (*Agent) ResumeSession

func (a *Agent) ResumeSession(ctx context.Context, params acp.ResumeSessionRequest) (resp acp.ResumeSessionResponse, err error)

ResumeSession restores a session without replaying its history.

func (*Agent) SetSessionConfigOption

func (a *Agent) SetSessionConfigOption(ctx context.Context, params acp.SetSessionConfigOptionRequest) (resp acp.SetSessionConfigOptionResponse, err error)

SetSessionConfigOption applies one select value.

func (*Agent) SetSessionMode

SetSessionMode exists because the SDK interface requires it. Native modes are config options, never ACP session modes.

func (*Agent) UnstableDeleteSession

func (a *Agent) UnstableDeleteSession(ctx context.Context, params acp.UnstableDeleteSessionRequest) (resp acp.UnstableDeleteSessionResponse, err error)

UnstableDeleteSession tombstones the session first, then closes any live session with the same id. Native state stays in opencode's home.

type ConcurrencyLimits

type ConcurrencyLimits struct {
	MaxActiveSessions        int
	MaxConcurrentClientCalls int
}

ConcurrencyLimits controls per-agent backpressure. Zero fields use defaults.

type ImageLimits

type ImageLimits struct {
	MaxInputBytesPerImage     int64
	MaxInputBytesPerPrompt    int64
	MaxOutputBytesPerImage    int64
	MaxOutputBytesPerToolCall int64
}

ImageLimits bounds decoded image bytes. A zero field disables that policy limit; the frame clamp still applies.

type OpenCodeOption

type OpenCodeOption func(*OpenCodeOptions)

OpenCodeOption configures OpenCodeOptions values.

func WithOpenCodeEffort

func WithOpenCodeEffort(level string) OpenCodeOption

WithOpenCodeEffort configures the reasoning level passed to opencode.

func WithOpenCodeEnv

func WithOpenCodeEnv(env map[string]string) OpenCodeOption

WithOpenCodeEnv configures the session environment overlay.

func WithOpenCodeExtraPathDirs

func WithOpenCodeExtraPathDirs(dirs ...string) OpenCodeOption

WithOpenCodeExtraPathDirs configures the directories prepended to the session PATH.

func WithOpenCodeMode

func WithOpenCodeMode(mode string) OpenCodeOption

WithOpenCodeMode selects a native agent.

func WithOpenCodeModel

func WithOpenCodeModel(model string) OpenCodeOption

WithOpenCodeModel configures the session model as "provider/id".

func WithOpenCodePermission

func WithOpenCodePermission(permission string) OpenCodeOption

WithOpenCodePermission selects native tool permission behavior.

type OpenCodeOptions

type OpenCodeOptions struct {
	// Mode selects a native agent.
	Mode string `json:"mode,omitempty"`
	// Permission selects ask, allow, or deny for native tools.
	Permission string `json:"permission,omitempty"`
	// Model selects the opencode model for this session as "provider/id".
	Model string `json:"model,omitempty"`
	// Env overlays the session's opencode process environment.
	Env map[string]string `json:"env,omitempty"`
	// ExtraPathDirs are absolute directories prepended, in order, to the PATH
	// of this session's opencode process.
	ExtraPathDirs []string `json:"extraPathDirs,omitempty"`
	// Effort is a reasoning-level value passed unchanged to opencode.
	Effort string `json:"effort,omitempty"`
}

OpenCodeOptions is the per-session options struct carried at _meta.opencode.options.

func NewOpenCodeOptions

func NewOpenCodeOptions(opts ...OpenCodeOption) OpenCodeOptions

NewOpenCodeOptions constructs OpenCodeOptions from functional options.

Example
package main

import (
	"fmt"

	opencodeacp "github.com/savid/acp-go-opencodev2"
)

func main() {
	options := opencodeacp.NewOpenCodeOptions(
		opencodeacp.WithOpenCodeModel("provider/model"),
		opencodeacp.WithOpenCodeEffort("high"),
	)

	fmt.Println(options.Model)
	fmt.Println(options.Effort)
}
Output:
provider/model
high

func (OpenCodeOptions) Meta

func (options OpenCodeOptions) Meta() map[string]any

Meta returns exactly {"opencode": {"options": {...}}} with the selected fields.

type Option

type Option func(*Options)

Option configures the opencode ACP agent.

func WithAgentName

func WithAgentName(name string) Option

WithAgentName sets the protocol identifier advertised during ACP initialize.

func WithAgentTitle

func WithAgentTitle(title string) Option

WithAgentTitle sets the human-readable agent name advertised during ACP initialize.

func WithAgentVersion

func WithAgentVersion(version string) Option

WithAgentVersion sets the agent version advertised during ACP initialize.

func WithConcurrencyLimits

func WithConcurrencyLimits(limits ConcurrencyLimits) Option

WithConcurrencyLimits sets process-local backpressure limits.

func WithConfiguredModels

func WithConfiguredModels(ids []string) Option

WithConfiguredModels names the models the host lists explicitly.

func WithDefaultModel

func WithDefaultModel(model string) Option

WithDefaultModel selects the model for new sessions as "provider/id".

func WithEnv

func WithEnv(env map[string]string) Option

WithEnv sets the static agent-scoped environment overlay applied to every opencode process after the inherited environment and before the session env.

func WithExecutablePath

func WithExecutablePath(path string) Option

WithExecutablePath selects the opencode executable.

func WithHome

func WithHome(path string) Option

WithHome sets opencode's native config root, passed to every session as the four XDG home variables.

func WithImageLimits

func WithImageLimits(limits ImageLimits) Option

WithImageLimits bounds decoded image bytes. A zero field disables that policy limit; a negative field fails construction.

func WithInputHandoffRoot

func WithInputHandoffRoot(dir string) Option

WithInputHandoffRoot sets the absolute directory under which handoff-form prompt images are read. The adapter never writes there.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger configures structured diagnostic logging.

func WithMeterProvider

func WithMeterProvider(provider metric.MeterProvider) Option

WithMeterProvider configures the OpenTelemetry meter provider.

func WithScratchDir

func WithScratchDir(dir string) Option

WithScratchDir sets the parent directory for ephemeral adapter state.

func WithSeedFiles

func WithSeedFiles(files map[string]string) Option

WithSeedFiles registers files written into opencode's config root before each launch. Keys are paths relative to that root; values are the contents.

func WithSessionStore

func WithSessionStore(store acpcore.SessionStore) Option

WithSessionStore configures the session store.

func WithTextMapPropagator

func WithTextMapPropagator(propagator propagation.TextMapPropagator) Option

WithTextMapPropagator configures trace-context extraction from ACP _meta.

func WithTracerProvider

func WithTracerProvider(provider trace.TracerProvider) Option

WithTracerProvider configures the OpenTelemetry tracer provider.

type Options

type Options struct {
	// AgentName is the protocol identifier advertised during ACP initialize.
	AgentName string
	// AgentTitle is the human-readable agent name advertised during ACP initialize.
	AgentTitle string
	// AgentVersion is the agent version advertised during ACP initialize.
	AgentVersion string

	// ExecutablePath selects the opencode executable. A bare name is searched on the
	// base PATH; a path containing a separator is used as given. Empty means
	// "opencode".
	ExecutablePath string
	// Home contains data, config, cache, and state directories mapped to the
	// corresponding XDG home variables. Empty leaves opencode to resolve its home from
	// the inherited environment exactly as it would from a shell.
	Home string
	// ScratchDir is the parent directory for ephemeral adapter state. Empty
	// means the system temp directory.
	ScratchDir string
	// InputHandoffRoot is the absolute directory under which handoff-form
	// prompt images are read. Empty rejects the handoff form.
	InputHandoffRoot string
	// DefaultModel selects the model for new sessions as "provider/id".
	DefaultModel string
	// ConfiguredModels are the model ids the host lists explicitly, each as
	// "provider/id".
	ConfiguredModels []string
	// Env is the static agent-scoped overlay on the inherited process
	// environment every opencode process runs with.
	Env map[string]string

	// Logger receives structured diagnostic logs. If nil, the default logger is used.
	Logger *slog.Logger
	// TracerProvider records adapter spans. If nil, tracing is a no-op.
	TracerProvider trace.TracerProvider
	// MeterProvider records adapter metrics. If nil, metrics are no-ops.
	MeterProvider metric.MeterProvider
	// TextMapPropagator extracts trace context from ACP _meta. If nil, W3C
	// trace context plus baggage propagation is used.
	TextMapPropagator propagation.TextMapPropagator

	// SessionStore is the durability boundary for session rows. Nil installs a
	// fresh in-memory store.
	SessionStore acpcore.SessionStore
	// ConcurrencyLimits controls process-local backpressure.
	ConcurrencyLimits ConcurrencyLimits
	// SeedFiles maps paths relative to opencode's config root to file contents
	// written there before each launch.
	SeedFiles map[string]string
	// ImageLimits bounds decoded image bytes on prompt input and emitted
	// output. Every field defaults to 6 MiB when the option is omitted.
	ImageLimits ImageLimits
	// contains filtered or unexported fields
}

Options configures the ACP agent process and the shared OpenCode HTTP server.

Directories

Path Synopsis
cmd
Package integration holds the tests that run against an installed OpenCode.
Package integration holds the tests that run against an installed OpenCode.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL