first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
// Package jsonrpc implements a minimal JSON-RPC 2.0 client over a subprocess's
// stdio (US-014/#116). It is the shared transport foundation reused by the MCP
// client (#130/#131), the plugin system (#132/#133) and process-isolated
// sub-agents (#135): each spawns an external executable and speaks line-delimited
// JSON-RPC 2.0 over the child's stdin/stdout.
//
// The wire format follows the spec: every message carries "jsonrpc":"2.0". A
// request has an id and expects a matching response; a notification omits the id
// and expects none. Requests and responses are correlated by id, so concurrent
// requests from different goroutines are safe — each waits only on its own reply.
//
// This file defines the message envelope and (de)serialization; transport.go
// implements the subprocess client.
package jsonrpc
import (
"encoding/json"
"fmt"
)
// Version is the only JSON-RPC protocol version this package speaks.
const Version = "2.0"
// ID is a JSON-RPC request identifier. The spec allows a string or a number;
// this client only ever generates numeric ids, but ID round-trips whatever a
// peer sends back so response correlation still works against servers that echo
// string ids.
type ID struct {
num int64
str string
isStr bool
}
// NumID returns a numeric request id.
func NumID(n int64) ID { return ID{num: n} }
// String renders the id for use as a map key when correlating responses.
func (id ID) String() string {
if id.isStr {
return "s:" + id.str
}
return fmt.Sprintf("n:%d", id.num)
}
// MarshalJSON emits the id as its underlying JSON scalar (number or string).
func (id ID) MarshalJSON() ([]byte, error) {
if id.isStr {
return json.Marshal(id.str)
}
return json.Marshal(id.num)
}
// UnmarshalJSON accepts either a JSON number or string id.
func (id *ID) UnmarshalJSON(data []byte) error {
var n int64
if err := json.Unmarshal(data, &n); err == nil {
id.num, id.isStr, id.str = n, false, ""
return nil
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
id.str, id.isStr, id.num = s, true, 0
return nil
}
return fmt.Errorf("jsonrpc: id is neither number nor string: %s", data)
}
// Request is an outgoing JSON-RPC request or notification. When ID is nil the
// message is a notification (no response expected).
type Request struct {
JSONRPC string `json:"jsonrpc"`
ID *ID `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// Response is an incoming JSON-RPC response. Exactly one of Result / Error is
// set on a well-formed reply.
type Response struct {
JSONRPC string `json:"jsonrpc"`
ID *ID `json:"id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
// Error is a JSON-RPC error object.
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
}
// Error implements the error interface so a peer error can be returned directly.
func (e *Error) Error() string {
return fmt.Sprintf("jsonrpc: server error %d: %s", e.Code, e.Message)
}
// newRequest builds a request (id != nil) or notification (id == nil) with the
// given params marshaled to JSON. A nil params value is omitted from the wire
// message.
func newRequest(id *ID, method string, params any) (*Request, error) {
req := &Request{JSONRPC: Version, ID: id, Method: method}
if params != nil {
raw, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("jsonrpc: marshal params for %q: %w", method, err)
}
req.Params = raw
}
return req, nil
}