acp

package
v0.0.21 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package acp is the Agent Client Protocol (ACP) adapter: it lets an ACP editor (Zed, and others that speak ACP) drive the mecatl harness as a subprocess over stdio. It is a THIRD wire format alongside gRPC and HTTP/SSE — its own JSON, NOT the proto contract — projecting the same domain session.Events onto the ACP session/update surface.

ACP is JSON-RPC 2.0 over stdio: the editor spawns the agent and the two speak over the agent's stdin/stdout. The adapter is therefore BOTH a JSON-RPC server (it handles inbound initialize / session/new / session/prompt requests and the session/cancel notification) AND a JSON-RPC client (it issues the outbound session/request_permission request and correlates the reply, and it pushes session/update notifications). The bidirectional codec lives in conn.go.

Layering: this is an ADAPTER. It consumes the surface-agnostic *server.Service (CreateSession / StartRun / Approve / Cancel) and the domain session types, exactly as the gRPC/HTTP adapters do. It is wired only in the composition root (cmd/mecated, behind the `acp` subcommand). It never imports contracts/gen: ACP carries its own JSON, decoupled from the proto.

fs/* DELEGATION (issue #2) — when the CLIENT advertises BOTH fs.readTextFile and fs.writeTextFile at initialize, a per-session workspace (fsworkspace.go) routes file Read/Write through the editor's buffers (fs/read_text_file / fs/write_text_file) instead of disk; Stat/Glob/Grep are composed from a local osfs view at the same root, and the Edit read-ledger is synthesized buffer-keyed over the delegated reads. When the caps are absent (or on session/load) it falls back to the osfs workspace rooted at the session cwd. See the ADR for the bounded hybrid's residual (grep sees disk) and the load asymmetry.

SCOPE — the following are DEFERRED to later phases and documented in docs/adr/0001-acp-adapter.md (Phase 2/3 landed diff blocks, allow_always rule learning, session/load + replay, modes, and slash commands — see the ADR):

  • grep/glob over editor BUFFERS (the fs/* hybrid searches disk) and fs/* delegation on session/load (a resumed session uses osfs).
  • DURABLE / broader-granularity learned permissions (today: in-memory, per-session, tool + exact-pattern only).
  • full-fidelity projection of turn.*/compaction events (dropped or folded into a thought/message chunk).

MULTIMODAL PROMPT CONTENT (issue #5) is DONE: session/prompt content blocks are translated by buildPromptContent into the prompt's flattened text PLUS media Parts (image/audio), reusing the session.NewContent validating constructors and per-prompt size caps at the ACP boundary. promptCapabilities now reflect the configured provider (ProviderCapabilities seam): image when the provider supports it, embeddedContext (inline-text resources flatten to text), audio wired-but-provider-gated (OpenAI Responses has no audio input member, so advertised false). A resource_link, an unsupported block type, or a media part the provider cannot consume is REJECTED loudly — never silently dropped.

PLAN APPROVAL (issue #206, Wave 4) — the ACP adapter has NO bespoke ApprovePlan method. ACP already composes the plan-approval flow from the two EXISTING primitives the editor speaks natively: session/set_mode (the operator picks default / accept-edits / plan) + session/prompt (the proceed message). The native gRPC ApprovePlan RPC and HTTP POST /v1/sessions/{id}/plan:approve are the headless COMPOSITION of those same two steps (resume the parked plan ask, flip the mode, start the continuation run) into one streamed response — a convenience an ACP editor does NOT need because it drives each step itself over its own session/* surface. A presented-plan permission.ask over ACP is resolved by the editor's existing session/request_permission reply, exactly as any other permission ask is; the subsequent mode flip + continuation prompt are ordinary session/set_mode + session/prompt calls. No new ACP method, no new capability.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Agent

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

Agent is the ACP adapter's request handler: it bridges the JSON-RPC Conn to the surface-agnostic *server.Service (the SAME service the gRPC/HTTP adapters consume). It owns the one-prompt-per-session serialization and the outbound request_permission round-trip. It is the ACP equivalent of server.HarnessServer.

func NewAgent

func NewAgent(svc *server.Service, opts ...AgentOption) *Agent

NewAgent constructs an Agent over svc. The Conn is set by Serve so the Agent can issue outbound request_permission calls and session/update notifications.

func (*Agent) Handle

func (a *Agent) Handle(ctx context.Context, method string, params json.RawMessage, isRequest bool) (any, error)

Handle is the JSON-RPC Handler: it routes inbound ACP methods. A request (isRequest true) returns a result/error; a notification (session/cancel) acts and returns nil. An unknown method returns codeMethodNotFound.

func (*Agent) Serve

func (a *Agent) Serve(ctx context.Context, conn *Conn) error

Serve runs the ACP stdio session over conn and blocks until the input stream ends or ctx is cancelled. It is the entry the composition root calls under `mecated acp` (passing a Conn over os.Stdin/os.Stdout).

ORDERING (load-bearing): Serve records conn as a.conn BEFORE conn.Serve starts dispatching inbound frames. a.conn is the outbound channel the handlers dereference — handleSessionPrompt's requestPermission issues an outbound Call, and notifyUpdate sends notifications, both through a.conn. Because the same conn handles inbound dispatch and a.conn is set first on this goroutine before any inbound frame is read, a handler can never observe a nil a.conn. Do not move the assignment after conn.Serve, and call Serve exactly once per Agent.

type AgentOption

type AgentOption func(*Agent)

AgentOption configures an Agent at construction.

func WithDiagnostics

func WithDiagnostics(d port.Diagnostics) AgentOption

WithDiagnostics injects the operational-logging sink the adapter writes its best-effort Debug lines through. A nil sink is ignored (NewAgent's NopDiagnostics default stands), so the adapter never nil-panics.

func WithResume

func WithResume(enabled bool) AgentOption

WithResume advertises session/load support (loadSession:true) and enables the session/load handler. The composition root passes true only when a durable session store is configured (mecated --store-dir), so resume is offered only when it can actually work.

type Conn

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

Conn is a bidirectional JSON-RPC 2.0 connection over a reader/writer pair (stdin/stdout for ACP). It is BOTH a server (it reads inbound requests and notifications and routes them to a Handler) AND a client (Call issues an outbound request and blocks for the correlated response; Notify sends a fire-and-forget notification). Writes are serialized by a mutex so concurrent session/update notifications and a request_permission round-trip never interleave a frame. It is safe for concurrent use.

func NewConn

func NewConn(r io.Reader, w io.Writer, h Handler) *Conn

NewConn builds a Conn over r/w with the given inbound Handler. The handler may be nil if the connection is used purely as a client (no inbound dispatch).

func (*Conn) Call

func (c *Conn) Call(ctx context.Context, method string, params, out any) error

Call issues an outbound request and blocks until the correlated response arrives, ctx is cancelled, or the connection closes. It is how the adapter issues session/request_permission to the editor. The result is unmarshalled into out (when non-nil); a JSON-RPC error response is returned as *rpcError.

func (*Conn) Notify

func (c *Conn) Notify(method string, params any) error

Notify sends a fire-and-forget notification (no id, no reply). It is how the adapter pushes session/update notifications to the editor.

func (*Conn) Serve

func (c *Conn) Serve(ctx context.Context) error

Serve runs the read loop until the input stream ends or ctx is cancelled. Each inbound frame is classified and routed: a response wakes the matching pending Call; a request is dispatched to the Handler on its own goroutine (so a Handler that itself issues an outbound Call — e.g. session/prompt issuing request_permission — does not deadlock the read loop); a notification is dispatched to the Handler with isRequest=false and produces no reply. Serve returns nil on a clean EOF, or the read/decode error otherwise.

type Handler

type Handler func(ctx context.Context, method string, params json.RawMessage, isRequest bool) (any, error)

Handler dispatches one inbound request or notification. For a request, the returned value is JSON-marshalled into the response Result; a returned error becomes a JSON-RPC error response (a *MethodError sets the code, anything else is codeInternalError). For a notification (isRequest false) the return value and error are ignored — a notification gets no reply per JSON-RPC.

type MethodError

type MethodError struct {
	Code    int
	Message string
}

MethodError is an error a Handler can return to control the JSON-RPC error code sent back to the peer (e.g. codeInvalidParams for a bad request).

func (*MethodError) Error

func (e *MethodError) Error() string

Jump to

Keyboard shortcuts

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