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
- func AllowAllPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
- func DenyAllPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
- func IsSessionNotFound(err error) bool
- func ReadOnlyPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
- type AbstainResult
- type AgentProc
- func (a *AgentProc) AuthMethods() []AuthMethod
- func (a *AgentProc) Authenticate(ctx context.Context, methodID, id, redirect string, cancel bool) (AuthResult, error)
- func (a *AgentProc) AvailableCommands() []CommandInfo
- func (a *AgentProc) Cancel(ctx context.Context, sid acp.SessionId) error
- func (a *AgentProc) Caps() Caps
- func (a *AgentProc) Close() error
- func (a *AgentProc) DropSession(sid acp.SessionId)
- func (a *AgentProc) ListSessions(ctx context.Context, cwd string) ([]SessionInfo, error)
- func (a *AgentProc) Models() (models []ModelInfo, currentID string)
- func (a *AgentProc) NewSession(ctx context.Context, cwd string, sink SessionUpdateSink, ...) (acp.SessionId, error)
- func (a *AgentProc) ProbeModels(ctx context.Context) error
- func (a *AgentProc) Prompt(ctx context.Context, sid acp.SessionId, prompt []acp.ContentBlock) (acp.StopReason, error)
- func (a *AgentProc) RebindSink(sid acp.SessionId, sink SessionUpdateSink)
- func (a *AgentProc) ReleaseSession(ctx context.Context, sid acp.SessionId) error
- func (a *AgentProc) ResumeSession(ctx context.Context, cwd string, sid acp.SessionId, sink SessionUpdateSink) error
- func (a *AgentProc) SetConfigOption(ctx context.Context, sid acp.SessionId, configID, value string) error
- func (a *AgentProc) SetModel(ctx context.Context, sid acp.SessionId, modelID string) error
- type AuthMethod
- type AuthResult
- type Caps
- type CommandInfo
- type Config
- type ModelInfo
- type PermissionFunc
- type PermissionPolicy
- type Prompter
- type RefuseConfig
- type RefuseResult
- type SessionInfo
- type SessionUpdateSink
- type ValidatingSink
- func (v *ValidatingSink) Commit(ctx context.Context) error
- func (v *ValidatingSink) CommitText(ctx context.Context, sid acp.SessionId, text string) error
- func (v *ValidatingSink) Drop()
- func (v *ValidatingSink) OnUpdate(ctx context.Context, n acp.SessionNotification) error
- func (v *ValidatingSink) Text() string
- type Validator
- type ValidatorFunc
Constants ¶
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 ¶
func AllowAllPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
AllowAllPermissions approves a request by selecting an allow-shaped option, falling back to the first option when no option advertises allow/approve.
func DenyAllPermissions ¶
func DenyAllPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
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
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 ¶
func ReadOnlyPermissions(_ context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
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 ¶
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) Close ¶
Close terminates the agent process. Returns after the process has exited (or been force-killed).
func (*AgentProc) DropSession ¶
DropSession removes the sink for a session.
func (*AgentProc) ListSessions ¶
ListSessions calls the unstable session/list. Caller must check Caps().ListSessions first.
func (*AgentProc) Models ¶
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 ¶
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
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 ¶
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 PermissionFunc ¶
type PermissionFunc func(context.Context, acp.RequestPermissionRequest) acp.RequestPermissionResponse
PermissionFunc adapts a function into a PermissionPolicy.
func (PermissionFunc) Decide ¶
func (f PermissionFunc) Decide(ctx context.Context, req acp.RequestPermissionRequest) acp.RequestPermissionResponse
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
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
func (v *ValidatingSink) OnUpdate(ctx context.Context, n acp.SessionNotification) error
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.