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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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