client

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package client wraps acp-go-sdk's low-level Connection for ACP relays. It manages a single stdio child agent process (e.g. fir --mode acp) and dispatches inbound server-initiated ACP calls — session updates, permission requests, fs reads/writes — back to the relay.

One AgentProc runs one ACP child process. It can serve many sessions concurrently — each NewSession/ResumeSession registers a per-session sink that receives the stream of session/update notifications.

The client talks to acp.Connection directly (rather than acp.ClientSideConnection) so it can issue the unstable session/list and session/resume methods that the SDK doesn't model. Standard methods are sent via acp.SendRequest with the SDK's typed request/response structs.

Security: the fs methods (ReadTextFile / WriteTextFile) currently require absolute paths but do not sandbox to the session cwd. That is adequate for trusted-agent relay deployments; do not expose this client to untrusted agents.

Defensive helpers for agent-spawn paths the production caller cannot trigger. Excluded from coverage via the `_must.go` suffix rule in .covignore.

Index

Constants

View Source
const SessionNotFoundCode = -32001

SessionNotFoundCode is the stable JSON-RPC error code an ACP agent returns when a request references a session it no longer holds in memory (released or idle-reaped). It is the shared contract between the agent (fir) and relays: on this code a relay can drop its cached session and re-create it.

Variables

This section is empty.

Functions

func AllowAllPermissions

AllowAllPermissions approves a request by selecting an allow-shaped option, falling back to the first option when no option advertises allow/approve.

func DenyAllPermissions

DenyAllPermissions rejects a request by selecting a reject-shaped option, falling back to the first option when no option advertises reject/deny.

func IsSessionNotFound added in v0.2.3

func IsSessionNotFound(err error) bool

IsSessionNotFound reports whether err is the typed ACP session-not-found error (JSON-RPC code SessionNotFoundCode). Relays use it to distinguish a recoverable "agent forgot this session" condition from other failures.

func ReadOnlyPermissions

ReadOnlyPermissions allows read-like tool calls and rejects everything else. Heuristic: if the tool call title contains a write/exec-shaped verb (write, edit, bash, exec, run, delete, rm), the request is rejected; otherwise it is allowed.

Types

type AbstainResult added in v0.2.6

type AbstainResult struct {
	// Stop is the stop reason of the turn.
	Stop acp.StopReason
	// Abstained is true when the agent declined to respond and nothing was
	// delivered downstream.
	Abstained bool
}

AbstainResult reports the outcome of PromptAbstainable.

func PromptAbstainable added in v0.2.6

func PromptAbstainable(ctx context.Context, agent Prompter, sid acp.SessionId, prompt []acp.ContentBlock, vs *ValidatingSink, sentinel string) (AbstainResult, error)

PromptAbstainable runs an ACP prompt through vs (which MUST be the session's sink) and lets the agent decline to respond. If the complete assistant message, trimmed, equals sentinel or is empty, the buffered output is discarded and nothing is delivered downstream (Abstained=true). Otherwise the buffered message is flushed downstream. Non-message updates (thoughts, plans, tool calls) stream live throughout — callers that must suppress those before the abstain verdict should gate them in the downstream sink.

The prompt always reaches the session regardless of the verdict, so the agent stays caught up on the conversation even when it stays silent. This is the generic decline construct for ambient participation: a transport-agnostic way for any ACP agent to opt out of replying via a sentinel string, with no tool plumbing. A blank sentinel disables sentinel-matching (only an empty message abstains).

type AgentProc

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

AgentProc wraps a single stdio-connected ACP agent process and the ACP connection driving it.

func Start

func Start(ctx context.Context, cfg Config) (*AgentProc, error)

Start launches the agent process, performs Initialize (capturing caps), and returns a ready-to-use AgentProc.

func (*AgentProc) AuthMethods

func (a *AgentProc) AuthMethods() []AuthMethod

AuthMethods returns the auth methods the agent advertised at Initialize. Empty if the agent didn't advertise any (or initialize hasn't run yet).

func (*AgentProc) Authenticate

func (a *AgentProc) Authenticate(ctx context.Context, methodID, id, redirect string, cancel bool) (AuthResult, error)

Authenticate invokes the ACP authenticate RPC. Modes:

  • id == "" && redirect == "" && !cancel : start an interactive login. The agent returns a fresh id (and URL) in AuthResult.
  • id != "" && redirect != "" : submit the pasted redirect.
  • id != "" && cancel == true : cancel that pending login.

methodID must match the id advertised in the initialize response (e.g. "oauth-anthropic"). Requires an agent that supports the _meta.auth.interactive extension; older agents will run the legacy blocking flow and may return an empty AuthResult.

func (*AgentProc) AvailableCommands added in v0.2.2

func (a *AgentProc) AvailableCommands() []CommandInfo

AvailableCommands returns a snapshot of the agent's last-advertised command catalog. Empty until the agent sends an availableCommandsUpdate. Safe for concurrent use.

func (*AgentProc) Cancel

func (a *AgentProc) Cancel(ctx context.Context, sid acp.SessionId) error

Cancel requests cancellation of an in-flight prompt for a session.

func (*AgentProc) Caps

func (a *AgentProc) Caps() Caps

Caps returns the agent's advertised capabilities (parsed at Initialize).

func (*AgentProc) Close

func (a *AgentProc) Close() error

Close terminates the agent process. Returns after the process has exited (or been force-killed).

func (*AgentProc) DropSession

func (a *AgentProc) DropSession(sid acp.SessionId)

DropSession removes the sink for a session.

func (*AgentProc) ListSessions

func (a *AgentProc) ListSessions(ctx context.Context, cwd string) ([]SessionInfo, error)

ListSessions calls the unstable session/list. Caller must check Caps().ListSessions first.

func (*AgentProc) Models

func (a *AgentProc) Models() (models []ModelInfo, currentID string)

Models returns a snapshot of the agent's last-seen available models. Empty until a session has been created (or ProbeModels has run).

func (*AgentProc) NewSession

func (a *AgentProc) NewSession(ctx context.Context, cwd string, sink SessionUpdateSink, systemPromptBlocks []acp.ContentBlock) (acp.SessionId, error)

NewSession creates a new ACP session and wires the given sink to receive its updates. Returns the ACP session id.

systemPromptBlocks, when non-nil, is sent in the request's _meta under "session.systemPrompt".blocks. Callers should only pass it when Caps().SystemPrompt is true; agents that haven't advertised the cap will simply ignore the unknown _meta key, but skipping it keeps the wire clean.

func (*AgentProc) ProbeModels

func (a *AgentProc) ProbeModels(ctx context.Context) error

ProbeModels creates a throwaway session in the agent's cwd to read its available-models list, then cancels it. The cached snapshot is returned from Models() afterwards. Idempotent: a no-op if Models() already has a list.

func (*AgentProc) Prompt

func (a *AgentProc) Prompt(ctx context.Context, sid acp.SessionId, prompt []acp.ContentBlock) (acp.StopReason, error)

Prompt sends a user message to the session. Returns the stop reason. The prompt is a sequence of ACP content blocks; callers build these from the latest user text plus any attachments.

func (*AgentProc) RebindSink

func (a *AgentProc) RebindSink(sid acp.SessionId, sink SessionUpdateSink)

RebindSink replaces the sink for an existing session id.

func (*AgentProc) ReleaseSession added in v0.2.3

func (a *AgentProc) ReleaseSession(ctx context.Context, sid acp.SessionId) error

ReleaseSession asks the agent to tear down and forget an in-memory session, freeing its extension/MCP subprocesses. The on-disk session is left intact. Returns a *acp.RequestError with code SessionNotFoundCode if the agent does not hold the session (see IsSessionNotFound).

func (*AgentProc) ResumeSession

func (a *AgentProc) ResumeSession(ctx context.Context, cwd string, sid acp.SessionId, sink SessionUpdateSink) error

ResumeSession calls the unstable session/resume and registers the sink for the resumed session. Caller must check Caps().ResumeSession first. The given sid is the agent-returned identifier (as listed by ListSessions).

func (*AgentProc) SetConfigOption

func (a *AgentProc) SetConfigOption(ctx context.Context, sid acp.SessionId, configID, value string) error

func (*AgentProc) SetModel

func (a *AgentProc) SetModel(ctx context.Context, sid acp.SessionId, modelID string) error

SetModel calls the unstable session/set_model RPC.

type AuthMethod

type AuthMethod struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Type        string `json:"type,omitempty"` // "agent" | "env_var" | "terminal" | ""
}

AuthMethod describes one authentication method advertised by the agent in the initialize response. Extra _meta fields the client doesn't use are ignored.

type AuthResult

type AuthResult struct {
	// State is one of "needs_redirect", "ok", "cancelled", or "" if the
	// agent's response carried no _meta.auth state field.
	State string
	// ID is the opaque pending-login id the agent returns on call 1.
	ID string
	// URL is the auth URL the user should visit (state="needs_redirect").
	URL string
	// Instructions is optional human-readable text alongside URL.
	Instructions string
}

AuthResult is the outcome of an Authenticate call.

type Caps

type Caps struct {
	// LoadSession is the standard agentCapabilities.loadSession bool.
	LoadSession bool
	// ListSessions reflects agentCapabilities.sessionCapabilities.list
	// (unstable RFD).
	ListSessions bool
	// ResumeSession reflects agentCapabilities.sessionCapabilities.resume
	// (unstable RFD).
	ResumeSession bool
	// EmbeddedContext reflects
	// agentCapabilities.promptCapabilities.embeddedContext: when true,
	// the relay may emit ContentBlock::Resource (with TextResourceContents)
	// in prompt requests instead of a bare ResourceLink, avoiding an
	// agent-side fetch.
	EmbeddedContext bool
	// SystemPrompt reflects agentCapabilities._meta["session.systemPrompt"].
	// When true the consumer may pass a system-prompt block list via
	// session/new._meta and the agent will treat it as durable across
	// compaction. When false the consumer must fall back to inlining the
	// same content on first prompt (and re-arm on resume).
	SystemPrompt bool
	// Extensions captures arbitrary entries from agentCapabilities._meta
	// other than the kit-owned "session.systemPrompt". Consumers can
	// probe for custom extension ids (e.g. "dev.acp-kit.status-line/v1")
	// to discover advertised support. Values are the raw JSON bytes of
	// the entry — typically `{}` or `{"version": N}`. nil when the
	// agent advertised no _meta.
	Extensions map[string]json.RawMessage
}

Caps captures the agent capabilities the relay cares about, parsed from the initialize response.

type CommandInfo added in v0.2.2

type CommandInfo struct {
	Name        string // command name, e.g. "reload" (invoked as "/reload")
	Description string
}

CommandInfo is one agent-advertised command (from an availableCommandsUpdate session notification).

type Config

type Config struct {
	// Command is the argv used to spawn the agent (e.g. []string{"fir", "--mode", "acp"}).
	Command []string
	// Cwd is the working directory for the child process.
	Cwd string
	// MCPServersForSession, when set, returns the MCP servers to include
	// in session/new and session/resume for a session at the given cwd.
	// This lets a client hand the agent client-hosted MCP servers (e.g. a
	// per-session stdio tool server) without changing the NewSession
	// signature. Nil (the default) means no MCP servers — identical to the
	// previous hardcoded empty list.
	MCPServersForSession func(cwd string) []acp.McpServer
	// Env is the environment for the child. If nil, os.Environ() is used.
	Env []string
	// Policy decides permission responses. If nil, AllowAllPermissions is used.
	Policy PermissionPolicy
	// CloseGrace is how long Close waits after SIGINT before SIGKILL. Default 2s.
	CloseGrace time.Duration
	// Stderr is where the child's stderr is forwarded. If nil, os.Stderr.
	Stderr io.Writer
	// ClientMeta carries extra entries to merge into the outgoing
	// clientCapabilities._meta map at Initialize. Use this to advertise
	// support for custom ACP extensions (keyed by extension id, e.g.
	// "dev.acp-kit.status-line/v1"). Keys collide last-wins with
	// kit-owned entries (e.g. "session.systemPrompt"); pick distinct
	// extension ids to avoid clobber.
	ClientMeta map[string]any
}

Config configures an AgentProc.

type ModelInfo

type ModelInfo struct {
	ID   string // "provider/modelID"
	Name string // human-readable label
}

ModelInfo is one entry in the agent's available-models list.

type PermissionFunc

PermissionFunc adapts a function into a PermissionPolicy.

func (PermissionFunc) Decide

Decide implements PermissionPolicy.

type PermissionPolicy

type PermissionPolicy interface {
	Decide(ctx context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
}

PermissionPolicy decides how to respond to session/request_permission.

type Prompter added in v0.2.4

type Prompter interface {
	Prompt(ctx context.Context, sid acp.SessionId, prompt []acp.ContentBlock) (acp.StopReason, error)
}

Prompter is the subset of an ACP agent client that PromptValidated drives. *AgentProc satisfies it.

type RefuseConfig added in v0.2.4

type RefuseConfig struct {
	// Validator decides whether the assistant message is acceptable.
	// If nil, output is always accepted (PromptValidated becomes a plain
	// buffered prompt).
	Validator Validator
	// MaxRefusals caps how many times the agent is re-prompted to
	// regenerate. 0 validates once and never re-prompts.
	MaxRefusals int
	// BuildReprompt builds the corrective prompt fed back to the agent
	// after a refusal. If nil, a single text block holding the reason is
	// used.
	BuildReprompt func(reason string) []acp.ContentBlock
	// Fallback transforms the rejected text into a deliverable form when
	// regeneration is exhausted (e.g. mechanically escape offending
	// tokens). If nil, the last (still-rejected) output is delivered as-is.
	Fallback func(text string) string
}

RefuseConfig configures PromptValidated.

type RefuseResult added in v0.2.4

type RefuseResult struct {
	Stop     acp.StopReason // stop reason of the delivered turn
	Refusals int            // number of regenerations triggered
	Accepted bool           // a generation passed validation
	FellBack bool           // Fallback was applied after exhaustion
}

RefuseResult reports the outcome of PromptValidated.

func PromptValidated added in v0.2.4

func PromptValidated(ctx context.Context, agent Prompter, sid acp.SessionId, prompt []acp.ContentBlock, vs *ValidatingSink, cfg RefuseConfig) (RefuseResult, error)

PromptValidated runs an ACP prompt, validates the assistant's complete message via vs (which MUST be the session's sink), and on refusal re-prompts the agent with a short reason up to cfg.MaxRefusals times. The visible message is delivered downstream only once accepted — or, on exhaustion, after cfg.Fallback. Non-message updates stream live throughout.

This is the generic "refuse LLM output" construct: a transport-agnostic way for an ACP client to reject and regenerate an agent's visible message before the user sees it, with a deterministic fallback so a turn can never wedge against a stubborn model.

type SessionInfo

type SessionInfo struct {
	SessionId string  `json:"sessionId"`
	Cwd       string  `json:"cwd,omitempty"`
	Title     *string `json:"title,omitempty"`
	UpdatedAt string  `json:"updatedAt,omitempty"`
}

SessionInfo is one entry from a session/list response.

type SessionUpdateSink

type SessionUpdateSink interface {
	OnUpdate(ctx context.Context, n acp.SessionNotification) error
}

SessionUpdateSink receives streaming updates for a single ACP session. Each consumer (e.g. a relay) implements this to forward the stream to its own transport (SSE, WebSocket, IM channel, ...).

type ValidatingSink added in v0.2.4

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

ValidatingSink wraps a downstream SessionUpdateSink. While a turn is in progress it buffers AgentMessageChunk updates (the assistant's visible message) instead of forwarding them, so an orchestrator can validate the complete message and either Commit it (flush downstream) or Drop it (discard) before the user ever sees it. Every other update — thoughts, tool calls, plans, command catalogs — passes through live, so progress keeps streaming.

Holding the message until validation is deliberate: a generic ACP transport cannot assume it can retract bytes already sent, so buffering the visible message is the only transport-agnostic way to guarantee that rejected output never reaches the user.

func NewValidatingSink added in v0.2.4

func NewValidatingSink(down SessionUpdateSink) *ValidatingSink

NewValidatingSink wraps down. Install the returned sink as the session's SessionUpdateSink (e.g. via AgentProc.NewSession) and drive turns through PromptValidated.

func (*ValidatingSink) Commit added in v0.2.4

func (v *ValidatingSink) Commit(ctx context.Context) error

Commit flushes the buffered message chunks downstream in order, then clears.

func (*ValidatingSink) CommitText added in v0.2.4

func (v *ValidatingSink) CommitText(ctx context.Context, sid acp.SessionId, text string) error

CommitText discards whatever was buffered and flushes a single synthetic message chunk carrying text. Used to deliver a transformed (e.g. escaped) fallback when regeneration is exhausted.

func (*ValidatingSink) Drop added in v0.2.4

func (v *ValidatingSink) Drop()

Drop discards buffered message chunks (used before a regeneration).

func (*ValidatingSink) OnUpdate added in v0.2.4

OnUpdate implements SessionUpdateSink. Agent message chunks are buffered; all other updates are forwarded downstream immediately.

func (*ValidatingSink) Text added in v0.2.4

func (v *ValidatingSink) Text() string

Text returns the assistant message text accumulated for the current attempt.

type Validator added in v0.2.4

type Validator interface {
	// Validate returns ok=true to accept the output. To refuse it, return
	// ok=false with a short, agent-facing reason describing what is wrong.
	// The reason is fed back to the agent as the next prompt so it can
	// regenerate a compliant message.
	Validate(text string) (reason string, ok bool)
}

Validator inspects the complete assistant message text produced during a single prompt turn and decides whether it may be delivered to the user.

type ValidatorFunc added in v0.2.4

type ValidatorFunc func(text string) (reason string, ok bool)

ValidatorFunc adapts a plain function to a Validator.

func (ValidatorFunc) Validate added in v0.2.4

func (f ValidatorFunc) Validate(text string) (string, bool)

Validate implements Validator.

Jump to

Keyboard shortcuts

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