Skip to content

ethpandaops/opencode-agent-sdk-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

opencode-agent-sdk-go

A Go SDK that spawns the opencode CLI in its Agent Client Protocol (ACP) mode and drives it over stdio JSON-RPC.

Built on top of coder/acp-go-sdk for the protocol layer. This package adds:

  • opencode subprocess management (spawn, version check, graceful shutdown)
  • an opinionated, functional-option API for sessions, prompts, model and mode selection
  • typed wrappers for opencode's unstable session RPCs (ForkSession, ResumeSession, UnstableSetModel) and the _meta.opencode.variant model-variant channel
  • generic session config switching via Session.SetConfigOption(ctx, configID, value) / SetConfigOptionBool — the canonical path behind SetModel / SetMode
  • Client.LoadSessionHistory — rehydrate a session and capture opencode's replayed session/update notifications into a typed SessionHistory (raw notifications, coalesced messages, last usage)
  • StatSession(ctx, sessionID, opts...) — client-less metadata lookup against opencode's local SQLite store for a single session (returns SessionStat without starting a subprocess)
  • ListSessions(ctx, ListSessionsOptions{}, opts...) — client-less enumeration of every session in that store, ordered newest-updated first; excludes archived sessions by default
  • typed session/update subscribers (Session.Subscribe + UpdateHandlers) for AgentMessage, Plan, ToolCall, Mode, Usage, etc.
  • turn-complete and updates-dropped hooks (WithOnTurnComplete, WithOnUpdateDropped)
  • cursor-paginated session iterator (Client.IterSessions)
  • a raw extension-method escape hatch (Client.CallExtension) for ACP _-prefixed methods the SDK doesn't wrap yet
  • session/request_permission and fs/write_text_file callbacks
  • observational cost + budget: CostTracker, BudgetTracker, WithMaxBudgetUSD (auto-cancels the in-flight turn when the cap is crossed), plus ErrBudgetExceeded
  • typed error classification: ClassifyError returns an ErrorClassification with coarse Class plus a finer SubClass (prompt-too-long, rate-limit-tokens vs requests, invalid-schema, invalid-model, subprocess-died) so resilience wrappers can pick targeted strategies
  • file-backed content helpers: PathInput (auto-detects image / audio / text / blob), PDFFileInput, AudioFileInput, ImageFileInput
  • in-process Go tools via a loopback HTTP MCP bridge (WithSDKTools) — no separate MCP server to run
  • opencode's terminal-auth auth-flow hint extraction
  • prompt-capability preflight (image/audio/embedded-resource blocks are rejected locally with ErrCapabilityUnavailable when the agent didn't advertise support)
  • OpenTelemetry metrics + spans under the opencodesdk.* namespace

Status

Early. Pinned to opencode CLI 1.14.20 and ACP protocol version 1. The API surface is still shifting between minor versions.

Requirements

  • Go 1.26+
  • opencode ≥ 1.14.20 in $PATH
  • A completed opencode auth login (credentials are read by opencode itself at session-start time)

Install

go get github.com/ethpandaops/opencode-agent-sdk-go

Quick start

One-shot via Query (plain text):

res, err := opencodesdk.Query(ctx, "Say hello in three words.", opencodesdk.WithCwd(cwd))
if err != nil {
    panic(err)
}
fmt.Println(res.AssistantText)

Multimodal via QueryContent:

img, _ := opencodesdk.ImageFileInput("./screenshot.png")

res, err := opencodesdk.QueryContent(ctx,
    opencodesdk.Blocks(
        opencodesdk.TextBlock("Describe the attached image in one sentence."),
        img,
    ),
    opencodesdk.WithCwd(cwd),
)

Dynamic prompt streams via QueryStreamContent + an iterator helper:

ch := make(chan []acp.ContentBlock)
go func() {
    defer close(ch)
    ch <- opencodesdk.Text("Reply with just: one")
    ch <- opencodesdk.Text("Reply with just: two")
}()

for res, err := range opencodesdk.QueryStreamContent(ctx,
    opencodesdk.PromptsFromChannel(ch),
    opencodesdk.WithCwd(cwd),
) {
    if err != nil {
        break
    }
    fmt.Println(res.AssistantText)
}

Long-lived client with streaming:

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    acp "github.com/coder/acp-go-sdk"
    opencodesdk "github.com/ethpandaops/opencode-agent-sdk-go"
)

func main() {
    cwd, _ := os.Getwd()

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
    defer cancel()

    err := opencodesdk.WithClient(ctx, func(c opencodesdk.Client) error {
        sess, err := c.NewSession(ctx)
        if err != nil {
            return err
        }

        go func() {
            for n := range sess.Updates() {
                if n.Update.AgentMessageChunk != nil && n.Update.AgentMessageChunk.Content.Text != nil {
                    fmt.Print(n.Update.AgentMessageChunk.Content.Text.Text)
                }
            }
        }()

        res, err := sess.Prompt(ctx, acp.TextBlock("Say hello in three words."))
        if err != nil {
            return err
        }

        fmt.Printf("\nstop: %s\n", res.StopReason)
        return nil
    }, opencodesdk.WithCwd(cwd))

    if err != nil {
        panic(err)
    }
}

In-process tools

Register a Go function as a tool and opencode can invoke it directly. The SDK runs a loopback HTTP MCP server for you, authenticates it with a random bearer token, and declares it in every session/new:

reverse := opencodesdk.NewTool(
    "reverse",
    "Reverse the characters of the input string.",
    map[string]any{
        "type": "object",
        "properties": map[string]any{
            "text": map[string]any{"type": "string"},
        },
        "required": []string{"text"},
    },
    func(ctx context.Context, in map[string]any) (opencodesdk.ToolResult, error) {
        text, _ := in["text"].(string)
        runes := []rune(text)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        return opencodesdk.ToolResult{Text: string(runes)}, nil
    },
)

c, _ := opencodesdk.NewClient(opencodesdk.WithSDKTools(reverse))

Closures are live: reach into DB handles, config, whatever the host process has. That's the entire reason to embed an agent inside a Go program versus shelling out.

Tool restrictions & system prompts

opencode's ACP surface deliberately exposes very few session-scoped knobs: only model and mode are configurable at runtime (Session.SetConfigOption). Two things the sister SDKs put on the session are not on opencode's wire:

  1. Per-session system prompts. session/new silently ignores a systemPrompt field. The canonical place for a custom system prompt is a custom agent defined in opencode.json (prompt: field). Every agent shows up in modes.availableModes, so you can select it at session start with opencodesdk.WithAgent("my-agent") or switch to it mid-session with Session.SetMode(ctx, "my-agent").

  2. Per-session tool allow/deny lists. The global permission ruleset in opencode.json (permission: { edit: "ask" }) is the only wire-level knob; custom agents can narrow tools further via their own tools: whitelist. On top of that the SDK exposes two convenience filters that short-circuit the permission callback:

    opencodesdk.WithAllowedTools("edit", "write"), // auto-approve
    opencodesdk.WithDisallowedTools("bash"),        // auto-reject
    opencodesdk.WithCanUseTool(myCallback),         // fallback for anything else

    Names match acp.ToolCall.Title (the opencode tool name, e.g. "edit", "bash", "read", "write"). These only fire when opencode's own rules resolve to "ask" — see WithCanUseTool for the opencode.json snippet that enables the ask path.

See examples/allowed_tools for a runnable version that sets up the ask-mode config and exercises all three layers together.

Options overview

Option Purpose
WithLogger(slog) structured logging
WithCwd(path) working directory for opencode + sessions
WithCLIPath(path) pin the opencode binary
WithCLIFlags(args...) extra flags passed to opencode acp
WithExtraArgs(map) map-shaped sister of WithCLIFlags; nil values render as bare --flag, non-nil as --flag=value
WithEnv(map) overlay on inherited env
WithStderr(fn) stderr callback
WithUser(id) tags OTel spans + metrics with a user attribute (multi-tenant attribution)
WithInitializeTimeout(d) handshake timeout (default 60s)
WithSkipVersionCheck(bool) skip the ≥1.14.20 assertion
WithModel(id) applied via session/set_config_option
WithAgent(name) sets the opencode mode (ModeBuild, ModePlan, ...)
WithInitialMode(id) ACP-terminology alias for WithAgent
WithEffort(level) maps an abstract EffortLow/Medium/High/Max enum onto opencode's per-model variant strings (/high, /xhigh, /max, …)
WithMaxTurns(n) client-side cap on assistant messages per session; auto-cancels when exceeded
WithMCPServers(servers...) external MCP servers
WithSDKTools(tools...) in-process tools via the bridge
WithCanUseTool(cb) permission-prompt callback
WithAllowedTools(names...) auto-approve named tools; skips WithCanUseTool
WithDisallowedTools(names...) auto-reject named tools; skips WithCanUseTool
WithOnFsWrite(cb) intercept fs/write_text_file
WithOnElicitation(cb) handle agent-initiated elicitation/create (ACP unstable); opencode 1.14.20 doesn't emit it yet — forward-compat stub
WithOnElicitationComplete(cb) observe elicitation/complete notifications for URL-mode elicitation
WithStrictCwdBoundary(bool) reject writes outside cwd
WithAddDirs(dirs...) extra workspace roots (ACP unstable, capability-gated)
WithPure() sugar for --pure — disables external opencode plugins
WithTransport(factory) custom transport (test doubles / embedded setups)
WithUpdatesBuffer(n) per-session update channel size
WithTerminalAuthCapability(bool) opt into opencode's terminal-auth launch hints
WithAutoLaunchLogin(bool) auto-spawn opencode auth login on authRequired
WithMeterProvider(mp) OTel MeterProvider
WithTracerProvider(tp) OTel TracerProvider

Utilities

A handful of utilities for common SDK workflows, mirrored against the claude and codex sister SDKs:

  • MCP tool-author helpersTextResult, ErrorResult, ImageResult, ParseArguments, SimpleSchema build tool results and input schemas without hand-rolled ToolResult literals.
  • Typed errors*CLINotFoundError, *ProcessError, *TransportError, and *RequestError carry structured diagnostic context (SearchedPaths, ExitCode, Stderr, JSON-RPC code + data) alongside the ErrCLINotFound, ErrClientClosed, ErrClientAlreadyConnected, ErrRequestTimeout, ErrTransport sentinels. All SDK-originated errors satisfy the OpencodeSDKError marker interface so callers can distinguish them from arbitrary Go errors with a single errors.As check.
  • Transport healthClient.GetTransportHealth() returns a TransportHealth snapshot with degradation flag, failure counts, and last-error details.
  • Session-cost trackerNewCostTracker() aggregates per-session cost and token usage from UsageUpdate notifications. LoadSessionCost / SaveSessionCost persist snapshots to $XDG_DATA_HOME/opencode/sdk/session-costs/<id>.json.
  • Session stat (client-less)StatSession(ctx, sessionID, opts...) reads metadata for a single session directly from opencode's local SQLite store at $XDG_DATA_HOME/opencode/opencode.db. Returns a SessionStat with project / slug / title / version / timestamps / archived state / message count without starting an opencode acp subprocess. Use WithCwd(path) to additionally scope the lookup by the session's recorded directory; WithOpencodeHome(...) overrides the XDG_DATA_HOME lookup. Returns ErrSessionNotFound when the row or the database file is missing.
  • Session list (client-less)ListSessions(ctx, ListSessionsOptions{}, opts...) reads every session from the same SQLite store, ordered by UpdatedAt descending. Archived sessions are excluded by default — set IncludeArchived: true to opt in, or Limit: N to cap the row count. WithCwd / WithOpencodeHome apply the same way as in StatSession. For an ACP-authoritative listing (sessions opencode itself can see for a given cwd), use Client.ListSessions / Client.IterSessions instead.
  • Structured outputDecodeStructuredOutput[T](result) pulls a typed T from QueryResult (session-update meta first, JSON-fenced assistant text second). WithOutputSchema(map[string]any) advises the agent via session/new._meta["structuredOutputSchema"].
  • Retry / classificationClassifyError(err) maps any SDK error to an ErrorClass + RecoveryAction. EvaluateRetry and ResilientQuery apply exponential back-off with jitter on retryable failures (rate limit, overload, transient connection).
  • Model catalogueListModels(ctx, opts...) returns every model opencode advertises for the configured cwd without writing a full session loop.
  • Model capabilitiesListModelCapabilities(ctx, opts...) returns per-model capability flags (Reasoning, Toolcall, Attachment, Interleaved, input/output modalities, context/output limits) keyed by "<providerID>/<modelID>". Spawns a short-lived opencode serve subprocess and calls /config/providers — ACP's model catalogue does not carry these flags, so a custom provider's reasoning: true in opencode.json is only visible through this helper.
  • Data-dir overrideWithOpencodeHome(path) sets XDG_DATA_HOME for the subprocess and for cost-snapshot persistence — convenient for tests and multi-env setups.
  • HooksWithHooks(...) registers typed callbacks for 11 lifecycle events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart/End, PermissionRequest/Denied, FileChanged, …). HookOutput{Continue:false} blocks the triggering action for the events that support blocking (UserPromptSubmit, PermissionRequest, FileChanged).
  • Tool-side elicitationElicit(ctx, params) callable from within a Tool.Execute sends an MCP elicitation through the loopback bridge back to opencode, which routes it to the user. Returns the user's answer or ErrElicitationUnavailable when there's no bound session.

See doc.go for full package-level documentation.

Observability

New metrics emitted alongside the existing opencodesdk.* surface:

  • opencodesdk.retry.attempt (class, outcome) — ResilientQuery retry decisions.
  • opencodesdk.structured_output.decode (source, outcome) — DecodeStructuredOutput invocations.
  • opencodesdk.transport.failure (kind) — transport-layer failures observed by the Client.

Prometheus

Two paths:

// 1. WithPrometheusRegisterer — SDK wires an OTel Prometheus
//    exporter internally.
reg := prometheus.NewRegistry()
_, _ = opencodesdk.Query(ctx, prompt,
    opencodesdk.WithPrometheusRegisterer(reg),
)

// 2. WithMeterProvider — bring your own OTel MeterProvider. Useful
//    when you're already running an OTel pipeline and want SDK
//    metrics to land alongside everything else.
opencodesdk.WithMeterProvider(myMeterProvider)

Wrap WithPrometheusRegisterer(reg) around a promhttp.HandlerFor(reg, ...) server in your own process to scrape SDK metrics via /metrics.

Examples

See examples/ for seven working programs:

  • quick_start — minimal round-trip
  • sdk_tools — in-process tool via the bridge
  • external_mcp — attach an external stdio MCP server via WithMCPServers
  • session_list — list prior sessions with pagination
  • permission_callback — interactive permission UX
  • fs_intercept — capture writes in memory instead of on disk
  • plan_modeWithInitialMode(ModePlan) to trigger permission prompts out of the box
  • cost_tracker — aggregate per-session cost and persist snapshots
  • resilient_query — ResilientQuery with backoff + error classification
  • hooks — typed lifecycle hooks via WithHooks
  • elicitation — a tool that asks the user to confirm via MCP elicitation through the loopback bridge

Architecture

your Go app
    │
    ▼
opencodesdk  (this package)
    │
    ▼
coder/acp-go-sdk   (JSON-RPC framing + schema types)
    │
    ▼  stdio
opencode acp   (child process)

In parallel:
your Go app ─(WithSDKTools)→ loopback HTTP MCP bridge ─←─ opencode

The SDK is deliberately a thin opinionated wrapper — we do not reimplement the ACP types or the JSON-RPC transport.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors