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 ¶
- type AdapterConn
- type AdapterProc
- type AdapterProfile
- type AdapterScope
- type AdapterSupervisor
- func (s *AdapterSupervisor) AddDesired(workDir string)
- func (s *AdapterSupervisor) GetConn(ctx context.Context, workDir string) (*AdapterConn, error)
- func (s *AdapterSupervisor) Nudge()
- func (s *AdapterSupervisor) RestartAll()
- func (s *AdapterSupervisor) Run(ctx context.Context)
- func (s *AdapterSupervisor) SetReconcileInterval(d time.Duration)
- func (s *AdapterSupervisor) SetSpawnFunc(fn SpawnFunc)
- func (s *AdapterSupervisor) StopAll()
- type Backend
- func (b *Backend) Abort(ctx context.Context) error
- func (b *Backend) ConfigOptions() []agent.ConfigOption
- func (b *Backend) Events() <-chan agent.Event
- func (b *Backend) Fork(ctx context.Context, messageID string) (agent.ForkResult, error)
- func (b *Backend) HandleRequestPermission(ctx context.Context, req sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error)
- func (b *Backend) HandleSessionUpdate(_ context.Context, n sdk.SessionNotification)
- func (b *Backend) Messages(ctx context.Context) ([]agent.MessageData, error)
- func (b *Backend) Models() (string, []agent.ModelInfo)
- func (b *Backend) Modes() (string, []agent.SessionMode)
- func (b *Backend) Open(ctx context.Context) error
- func (b *Backend) OpenAndSend(ctx context.Context, opts agent.SendMessageOpts) error
- func (b *Backend) PendingPermissions() []agent.PermissionData
- func (b *Backend) RespondPermission(ctx context.Context, permissionID string, allow bool, denyMessage string) error
- func (b *Backend) RespondQuestion(ctx context.Context, requestID string, answers []agent.QuestionAnswer, ...) error
- func (b *Backend) Revert(ctx context.Context, messageID string) error
- func (b *Backend) Send(ctx context.Context, opts agent.SendMessageOpts) error
- func (b *Backend) SessionID() string
- func (b *Backend) Status() agent.SessionStatus
- func (b *Backend) Stop() error
- type ConnResolver
- type SessionHandler
- type SpawnFunc
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 ¶
func (c *AdapterConn) Conn() *sdk.ClientSideConnection
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 ¶
func (c *AdapterConn) Init() sdk.InitializeResponse
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 ¶
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) Fork ¶
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) Models ¶
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 ¶
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 ¶
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) Send ¶
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) Status ¶
func (b *Backend) Status() agent.SessionStatus
Status returns the current session status snapshot.
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).
Source Files
¶
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. |