Vectrum
Sign In
87 lines · 3.2 KB · Go
// Package acp speaks the Agent Client Protocol: JSON-RPC 2.0 framed as one
// compact object per line, over a pair of streams that are usually this
// process's stdin and stdout.
//
// It is a peer, not a client or a server. The other end calls initialize,
// opens sessions and sends prompts; this end calls back to ask permission for
// a tool. Both directions run over the same connection at the same time, which
// is why the read loop dispatches every inbound message onto its own goroutine
// — a permission request answered on the read loop would be a deadlock with
// the peer doing the same thing.
//
// The package knows nothing about agents, models or tools. It is the wire and
// the vocabulary, and [Handler] is where somebody else's meaning attaches.
package acp

import (
	"encoding/json"
	"fmt"
)

// jsonrpcVersion is required on every frame in both directions. The clients
// that matter validate it as a literal, so omitting it is not a shortcut, it
// is a decode error at the far end.
const jsonrpcVersion = "2.0"

// Message is one frame. The four payload fields are mutually exclusive in
// practice — a request has Method, a response has Result or Error — but they
// share a struct because that is how they share a line.
type Message struct {
	JSONRPC string `json:"jsonrpc"`
	// ID is kept raw so a string id and a numeric one both come back exactly
	// as they were sent. A response whose id does not match byte for byte is a
	// response the peer cannot pair up.
	ID     *json.RawMessage `json:"id,omitempty"`
	Method string           `json:"method,omitempty"`
	Params json.RawMessage  `json:"params,omitempty"`
	Result json.RawMessage  `json:"result,omitempty"`
	Error  *Error           `json:"error,omitempty"`
}

// The JSON-RPC error codes this package produces.
const (
	CodeParse          = -32700
	CodeInvalidRequest = -32600
	CodeMethodNotFound = -32601
	CodeInvalidParams  = -32602
	CodeInternal       = -32603
)

// Error is a JSON-RPC error object, and also a Go error: a [Handler] that
// returns one chooses the code the peer sees, and anything else becomes
// CodeInternal.
type Error struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    any    `json:"data,omitempty"`
}

func (e *Error) Error() string { return fmt.Sprintf("acp: %s (%d)", e.Message, e.Code) }

// Errorf builds an [Error] with a formatted message.
func Errorf(code int, format string, args ...any) *Error {
	return &Error{Code: code, Message: fmt.Sprintf(format, args...)}
}

// asError turns whatever a handler returned into something the peer can be
// told. An error that already chose its code keeps it.
func asError(err error) *Error {
	if e, ok := err.(*Error); ok {
		return e
	}
	return &Error{Code: CodeInternal, Message: err.Error()}
}

// decodeParams unmarshals a request's params, reporting a bad shape as
// CodeInvalidParams rather than as a server fault. Absent params decode as the
// zero value: a method whose arguments are all optional may be called with
// none.
func decodeParams(raw json.RawMessage, v any) error {
	if len(raw) == 0 || string(raw) == "null" {
		return nil
	}
	if err := json.Unmarshal(raw, v); err != nil {
		return Errorf(CodeInvalidParams, "cannot decode params: %v", err)
	}
	return nil
}