acp

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package acp adapts Agent Client Protocol (ACP) agents to clank's SessionBackend seam. An AdapterSupervisor spawns and supervises adapter processes (opencode acp, claude-agent-acp, codex-acp) and owns their stdio JSON-RPC connections; per-adapter variance lives in AdapterProfile values, so one implementation serves every ACP agent.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AdapterConn

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

AdapterConn wraps one adapter process's stdio JSON-RPC connection: initialize runs once at construction, the response (capabilities) is cached, and session/update + session/request_permission traffic is routed to the SessionHandler registered for the session id.

func NewAdapterConn

func NewAdapterConn(ctx context.Context, profile AdapterProfile, stdin io.Writer, stdout io.Reader, logf func(string, ...any)) (*AdapterConn, error)

NewAdapterConn binds the child's stdin/stdout pipes, runs initialize, and starts routing. It does not own the process — the supervisor does. Exported so acptest can build in-process procs over pipe pairs.

func (*AdapterConn) Closed

func (c *AdapterConn) Closed() <-chan struct{}

Closed is closed when the peer disconnects or the process dies.

func (*AdapterConn) Conn

Conn exposes the underlying connection for outbound RPCs (session/new, session/prompt, …).

func (*AdapterConn) Deregister

func (c *AdapterConn) Deregister(id sdk.SessionId)

Deregister stops routing for a session id.

func (*AdapterConn) Init

Init returns the cached initialize response (agent capabilities).

func (*AdapterConn) Register

func (c *AdapterConn) Register(id sdk.SessionId, h SessionHandler)

Register routes a session's server→client traffic to h.

type AdapterProc

type AdapterProc struct {
	Conn *AdapterConn
	// Stop terminates the process: graceful first, then forceful after
	// stopGrace. Must be safe to call more than once.
	Stop func()
	// contains filtered or unexported fields
}

AdapterProc is one supervised adapter process (or its in-process test stand-in): a live conn plus a stop hook. Spawn functions produce it.

type AdapterProfile

type AdapterProfile struct {
	// ID is a stable identifier used in logs and the software manifest.
	ID string
	// Backend is the clank backend type this profile serves.
	Backend agent.BackendType
	Scope   AdapterScope
	// Prepare provisions whatever Command/Env need for scopeDir (e.g.
	// installing the adapter package, materializing guidance) before each
	// spawn attempt. Must be idempotent and cheap once satisfied; it owns
	// its own timeout budget. nil = ready.
	Prepare func(ctx context.Context, scopeDir string) error
	// Command returns the argv that launches the adapter for scopeDir
	// (empty for ScopeHost profiles). Called after Prepare succeeded.
	Command func(scopeDir string) (bin string, args []string)
	// Env returns credential/config env vars for scopeDir, merged over
	// the parent environment at spawn. A change in the returned map
	// restarts that scope's adapter on the next reconcile (fingerprint).
	Env func(scopeDir string) map[string]string
	// SessionNewMeta builds session/new's _meta payload; guidance is the
	// assembled system prompt for fresh sessions ("" on resume). nil =
	// no meta (the adapter has no session-level injection channel).
	SessionNewMeta func(guidance string) map[string]any
	// ModelOption maps a model override onto a session config option
	// (option id + value). nil = overrides are ignored for this adapter.
	//
	// Session modes need no profile hook: the agent owns its mode
	// vocabulary — clank passes mode ids through to session/set_mode and
	// surfaces the advertised list untranslated.
	ModelOption func(o agent.ModelOverride) (id, value string, ok bool)
}

AdapterProfile is the per-adapter variance consumed by the supervisor and conn layers: how to launch the process, at what scope, and with which environment. Turn-level variance (session/new _meta, mode maps, guidance strategy) is added by the backend slices that need it.

func ClaudeProfile

func ClaudeProfile(bunBin, adapterEntry string, env func(string) map[string]string) AdapterProfile

ClaudeProfile serves the claude-code backend through claude-agent-acp run as plain JS under the pinned bun. One process per host — the adapter spawns one Claude CLI per session internally, and the Agent SDK's bundled native CLI is the pinned agent. Guidance rides session/new's _meta.systemPrompt as a preset append (the adapter forwards it to the SDK). Credentials arrive via the manager's env resolver (CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY); running as root (sprites) additionally needs IS_SANDBOX=1 or the adapter blocks bypassPermissions.

func CodexProfile

func CodexProfile(bunBin, adapterEntry string, env func(string) map[string]string) AdapterProfile

CodexProfile serves the codex backend through the codex-acp adapter (npm @agentclientprotocol/codex-acp) run as plain JS under the pinned bun. One process per host: the adapter keeps a single codex app-server child that multiplexes every session as a thread; cwd is a per-session session/new parameter. env carries CODEX_API_KEY (nil = let codex fall back to its own ChatGPT login in ~/.codex). Modes are agent-owned: codex advertises its approval/sandbox presets (read-only / agent / agent-full-access) and clank passes the chosen id straight through.

func OpenCodeProfile

func OpenCodeProfile(bin string) AdapterProfile

OpenCodeProfile serves the opencode backend through `opencode acp`, one process per project dir (the subcommand boots a full opencode server bound to its cwd). bin is the opencode executable — the user's own install by design: their binary, their state, no version skew clank can introduce. Credentials ride opencode's auth store, so Env is nil.

func (AdapterProfile) ScopeKey

func (p AdapterProfile) ScopeKey(workDir string) string

ScopeKey maps a session workDir onto the supervisor's process key.

type AdapterScope

type AdapterScope int

AdapterScope declares how many adapter processes a profile needs.

const (
	// ScopeHost runs one adapter process for the whole host; sessions carry
	// their own cwd (codex-acp app-server, claude-agent-acp).
	ScopeHost AdapterScope = iota
	// ScopePerDir runs one adapter process per project directory
	// (opencode acp boots a full opencode server bound to its cwd).
	ScopePerDir
)

type AdapterSupervisor

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

AdapterSupervisor reconciles desired adapter processes, one per scope key. Modeled on OpenCodeServerManager: a single Run goroutine owns all process starts/stops; everything else registers desire and waits.

func NewAdapterSupervisor

func NewAdapterSupervisor(profile AdapterProfile, logf func(string, ...any)) (*AdapterSupervisor, error)

NewAdapterSupervisor validates the profile and prepares a supervisor; call Run to start reconciling.

func (*AdapterSupervisor) AddDesired

func (s *AdapterSupervisor) AddDesired(workDir string)

AddDesired marks workDir's scope as wanted without waiting for it.

func (*AdapterSupervisor) GetConn

func (s *AdapterSupervisor) GetConn(ctx context.Context, workDir string) (*AdapterConn, error)

GetConn returns a live conn for workDir's scope, starting the adapter if needed. Blocks until the reconciler delivers one or ctx ends.

func (*AdapterSupervisor) Nudge

func (s *AdapterSupervisor) Nudge()

Nudge asks the reconciler to run promptly (e.g. after a credential write changed the profile env). Non-blocking.

func (*AdapterSupervisor) RestartAll

func (s *AdapterSupervisor) RestartAll()

RestartAll stops every running adapter while keeping the desired set, so the reconciler respawns them with fresh Prepare/Env — the ACP analog of OpenCode's restart-on-credential-write. Backends observe the transport loss, go dead, and rehydrate lazily via ensureBackend.

func (*AdapterSupervisor) Run

func (s *AdapterSupervisor) Run(ctx context.Context)

Run is the reconciler loop and the only goroutine that starts or stops adapter processes. Blocks until ctx is cancelled, then stops everything.

func (*AdapterSupervisor) SetReconcileInterval

func (s *AdapterSupervisor) SetReconcileInterval(d time.Duration)

SetReconcileInterval shortens the reconcile cadence — test hook.

func (*AdapterSupervisor) SetSpawnFunc

func (s *AdapterSupervisor) SetSpawnFunc(fn SpawnFunc)

SetSpawnFunc replaces process launching — test hook.

func (*AdapterSupervisor) StopAll

func (s *AdapterSupervisor) StopAll()

StopAll terminates every adapter, fails pending waiters, and clears desired state. The supervisor is unusable afterwards.

type Backend

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

Backend adapts one ACP session to agent.SessionBackend. One value per clank session; the adapter process behind it is shared and supervised.

func NewBackend

func NewBackend(profile AdapterProfile, workDir, resumeExternalID, guidance string, lastConfig map[string]string, resolver ConnResolver, logf func(string, ...any)) *Backend

NewBackend builds a SessionBackend for one clank session. resumeExternalID != "" resumes an existing ACP session via session/load; guidance is injected only on fresh sessions. lastConfig is the session's last-applied config, re-asserted on resume (see Backend.lastConfig).

func (*Backend) Abort

func (b *Backend) Abort(ctx context.Context) error

Abort cancels the in-flight turn (session/cancel), drops queued prompts, and resolves parked permissions as cancelled. The prompt response arrives with stopReason=cancelled and settles to idle.

func (*Backend) ConfigOptions

func (b *Backend) ConfigOptions() []agent.ConfigOption

ConfigOptions implements agent.ConfigOptionsReporter: the agent's full advertised config knobs, untranslated. Deep-cloned (including each option's Values) so callers can't mutate retained backend state.

func (*Backend) Events

func (b *Backend) Events() <-chan agent.Event

Events returns the backend's event stream (hub relay is the sole drain).

func (*Backend) Fork

func (b *Backend) Fork(ctx context.Context, messageID string) (agent.ForkResult, error)

Fork branches the session where the adapter advertises the unstable fork capability. ACP fork has no message anchor, so only tip forks are honest — a mid-history messageID gets a typed unsupported error instead of silently forking the whole tip.

func (*Backend) HandleRequestPermission

func (b *Backend) HandleRequestPermission(ctx context.Context, req sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error)

HandleRequestPermission implements SessionHandler: park the agent's request, surface EventPermission, and block until RespondPermission (or an abort/stop/disconnect sweep) decides — the same parking contract as the bespoke claude CanUseTool bridge.

func (*Backend) HandleSessionUpdate

func (b *Backend) HandleSessionUpdate(_ context.Context, n sdk.SessionNotification)

HandleSessionUpdate implements SessionHandler: reduce, then emit.

func (*Backend) Messages

func (b *Backend) Messages(ctx context.Context) ([]agent.MessageData, error)

Messages snapshots the in-memory transcript (committed + in-flight).

func (*Backend) Models

func (b *Backend) Models() (string, []agent.ModelInfo)

Models implements agent.ModelReporter: the agent-advertised model choices for this session plus the active one.

func (*Backend) Modes

func (b *Backend) Modes() (string, []agent.SessionMode)

Modes implements agent.ModeReporter: the agent-advertised session modes plus the currently active id, untranslated. currentMode is maintained by session/new + session/load responses, applyMode, and live current_mode_update notifications (HandleSessionUpdate).

func (*Backend) Open

func (b *Backend) Open(ctx context.Context) error

Open establishes the ACP session: session/new for fresh sessions, session/load (full replay into the reducer, no events) for resumes. Idempotent; safe to call on every dispatch like the host does.

func (*Backend) OpenAndSend

func (b *Backend) OpenAndSend(ctx context.Context, opts agent.SendMessageOpts) error

OpenAndSend is Open followed by Send — ACP has no fused primitive.

func (*Backend) PendingPermissions

func (b *Backend) PendingPermissions() []agent.PermissionData

PendingPermissions implements agent.PendingPermissionsReporter: the requests currently parked in HandleRequestPermission, oldest first, so a client that (re)joins mid-block can re-render the prompt it never saw on the live stream.

func (*Backend) RespondPermission

func (b *Backend) RespondPermission(ctx context.Context, permissionID string, allow bool, denyMessage string) error

RespondPermission resolves a parked request. A denyMessage becomes a follow-up prompt: ACP permission outcomes carry an option id and nothing else, so the reason reaches the model as the user's next message. That is how plan revision works — rejecting ExitPlanMode keeps the session in plan mode and ends the turn, and the queued message asks for the changes. Ignored when allow is true (a granted permission has no reason to carry).

func (*Backend) RespondQuestion

func (b *Backend) RespondQuestion(ctx context.Context, requestID string, answers []agent.QuestionAnswer, reject bool) error

RespondQuestion is an approved cut under ACP (AskUserQuestion retired); the backend never emits Part.Question, so no prompt can arrive.

func (*Backend) Revert

func (b *Backend) Revert(ctx context.Context, messageID string) error

Revert is an approved cut under ACP (no protocol support).

func (*Backend) Send

func (b *Backend) Send(ctx context.Context, opts agent.SendMessageOpts) error

Send resolves attachments, applies mode/model changes, records + emits the user message, and enqueues the prompt for the turn runner. ACP is one-turn-at-a-time, so prompts dispatch sequentially.

func (*Backend) SessionID

func (b *Backend) SessionID() string

SessionID returns the backend-native (ACP) session id, "" until known.

func (*Backend) Status

func (b *Backend) Status() agent.SessionStatus

Status returns the current session status snapshot.

func (*Backend) Stop

func (b *Backend) Stop() error

Stop tears the backend down: best-effort session/close (frees the claude adapter's per-session CLI child), deregister, final dead status. The adapter process itself stays up — the supervisor owns it.

type ConnResolver

type ConnResolver func(ctx context.Context) (*AdapterConn, error)

ConnResolver returns a live conn for the backend's scope — the supervisor's GetConn wrapped by the manager (analog of ServerResolver).

type SessionHandler

type SessionHandler interface {
	HandleSessionUpdate(ctx context.Context, n sdk.SessionNotification)
	HandleRequestPermission(ctx context.Context, req sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error)
}

SessionHandler receives the server→client traffic for one ACP session. The acp.Backend implements it (slice 3); tests implement it directly.

type SpawnFunc

type SpawnFunc func(ctx context.Context, scopeDir string) (*AdapterProc, error)

SpawnFunc launches one adapter for scopeDir. The default implementation execs the profile's Command; tests substitute in-process pipe pairs (the same seam OpenCodeServerManager exposes via SetStartServerFn).

Directories

Path Synopsis
Package acptest provides an in-process scripted ACP agent for tests: a real agent speaking real JSON-RPC over real pipes through the same SDK clank uses — the protocol-level analog of hosttest.StubBackend.
Package acptest provides an in-process scripted ACP agent for tests: a real agent speaking real JSON-RPC over real pipes through the same SDK clank uses — the protocol-level analog of hosttest.StubBackend.

Jump to

Keyboard shortcuts

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